001package edu.pdx.cs410J.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import com.google.common.io.ByteStreams; 005import edu.pdx.cs410J.ParserException; 006import edu.pdx.cs410J.grader.gradebook.Grade; 007import edu.pdx.cs410J.grader.gradebook.GradeBook; 008import edu.pdx.cs410J.grader.gradebook.Student; 009import edu.pdx.cs410J.grader.gradebook.XmlGradeBookParser; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013import java.io.*; 014import java.time.LocalDateTime; 015import java.util.*; 016import java.util.jar.Attributes; 017import java.util.regex.Matcher; 018import java.util.regex.Pattern; 019import java.util.stream.Stream; 020import java.util.zip.ZipEntry; 021import java.util.zip.ZipInputStream; 022 023public class AndroidZipFixer { 024 025 private static final Logger logger = LoggerFactory.getLogger("edu.pdx.cs410J.grader"); 026 027 private final GradeBook gradeBook; 028 029 @VisibleForTesting 030 AndroidZipFixer(GradeBook book) { 031 this.gradeBook = book; 032 } 033 034 public static void main(String[] args) { 035 String gradeBookFileName = null; 036 String outputDirectoryName = null; 037 List<String> zipFileNames = new ArrayList<>(); 038 039 for (String arg : args) { 040 if (gradeBookFileName == null) { 041 gradeBookFileName = arg; 042 043 } else if (outputDirectoryName == null) { 044 outputDirectoryName = arg; 045 046 } else { 047 zipFileNames.add(arg); 048 } 049 } 050 051 if (gradeBookFileName == null) { 052 usage("Missing grade book file name"); 053 return; 054 } 055 056 if (outputDirectoryName == null) { 057 usage("Missing output directory"); 058 return; 059 } 060 061 if (zipFileNames.isEmpty()) { 062 usage("Missing zip file"); 063 return; 064 } 065 066 File outputDirectory = new File(outputDirectoryName); 067 GradeBook book; 068 try { 069 XmlGradeBookParser parser = new XmlGradeBookParser(gradeBookFileName); 070 book = parser.parse(); 071 072 } catch (IOException | ParserException ex) { 073 exitWithExceptionMessage("While parsing grade book", ex); 074 return; 075 } 076 AndroidZipFixer fixer = new AndroidZipFixer(book); 077 078 for (String zipFileName : zipFileNames) { 079 File zipFile = new File(zipFileName); 080 try { 081 fixer.fixZipFile(zipFile, outputDirectory); 082 083 } catch (IOException e) { 084 exitWithExceptionMessage("While fixing zip file " + zipFile, e); 085 } 086 087 } 088 } 089 090 private static void exitWithExceptionMessage(String message, Exception ex) { 091 System.err.println("+++ " + message); 092 System.err.println(ex.getMessage()); 093 094 logger.debug(message, ex); 095 096 System.exit(1); 097 } 098 099 private void fixZipFile(File zipFile, File outputDirectory) throws IOException { 100 File fixedZipFile = getFixedZipFile(zipFile, outputDirectory); 101 String studentId = getStudentIdFromZipFileName(zipFile); 102 FileOutputStream fixZipStream = new FileOutputStream(fixedZipFile); 103 104 InputStream zipStream = new FileInputStream(zipFile); 105 106 HashMap<Attributes.Name, String> manifestEntries = getManifestEntriesForStudent(studentId); 107 fixZipFile(zipStream, fixZipStream, manifestEntries); 108 } 109 110 @VisibleForTesting 111 void fixZipFile(InputStream zipStream, OutputStream fixedZipStream, HashMap<Attributes.Name, String> manifestEntries) throws IOException { 112 ZipFileMaker maker = new ZipFileMaker(fixedZipStream, manifestEntries); 113 try ( 114 ZipInputStream input = new ZipInputStream(zipStream) 115 ) { 116 Map<ZipEntry, InputStream> zipFileEntries = new HashMap<>(); 117 118 for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input.getNextEntry()) { 119 String entryName = entry.getName(); 120 String fixedEntryName = getFixedEntryName(entryName); 121 122 if (fixedEntryName != null) { 123 logger.debug(entryName + " fixed to " + fixedEntryName); 124 125 ZipEntry fixedEntry = new ZipEntry(fixedEntryName); 126 fixedEntry.setLastModifiedTime(entry.getLastModifiedTime()); 127 fixedEntry.setMethod(ZipEntry.DEFLATED); 128 129 byte[] entryBytes = ByteStreams.toByteArray(input); 130 zipFileEntries.put(fixedEntry, new ByteArrayInputStream(entryBytes)); 131 132 } else { 133 logger.debug(entryName + " ignored"); 134 } 135 } 136 137 maker.makeZipFile(zipFileEntries); 138 } 139 } 140 141 private String getStudentIdFromZipFileName(File zipFile) { 142 String fileName = zipFile.getName(); 143 int index = fileName.indexOf(".zip"); 144 if (index < 0) { 145 return fileName; 146 147 } else { 148 return fileName.substring(0, index); 149 } 150 } 151 152 @VisibleForTesting 153 HashMap<Attributes.Name, String> getManifestEntriesForStudent(String studentId) { 154 Student student = 155 this.gradeBook.getStudent(studentId).orElseThrow(() -> new IllegalArgumentException("Unknown student: " + studentId)); 156 157 HashMap<Attributes.Name, String> manifest = new HashMap<>(); 158 manifest.put(Submit.ManifestAttributes.USER_ID, student.getId()); 159 manifest.put(Submit.ManifestAttributes.USER_NAME, student.getFullName()); 160 161 LocalDateTime submissionTime = getSubmissionTime(student); 162 if (submissionTime != null) { 163 manifest.put(Submit.ManifestAttributes.SUBMISSION_TIME, Submit.ManifestAttributes.formatSubmissionTime(submissionTime)); 164 } 165 166 return manifest; 167 } 168 169 private LocalDateTime getSubmissionTime(Student student) { 170 Grade project = student.getGrade("Project5"); 171 if (project == null) { 172 logger.warn("No Project5 submission for " + student.getId()); 173 return null; 174 } 175 176 return project.getSubmissionTimes().stream().max(Comparator.naturalOrder()).orElse(null); 177 } 178 179 @VisibleForTesting 180 static String getFixedEntryName(String entryName) { 181 Stream<String> ignore = Stream.of("__MACOSX", "/test", "/androidTest", "/build/", ".DS_Store", "gradlew.bat"); 182 if (ignore.anyMatch(entryName::contains)) { 183 return null; 184 } 185 186 List<String> moveToTopLevel = 187 List.of("app/build.gradle", "build.gradle", "gradle.properties", "gradlew", "settings.gradle" ); 188 for (String special : moveToTopLevel) { 189 if (entryName.contains(special)) { 190 return special; 191 } 192 } 193 194 if (entryName.endsWith("gradle")) { 195 return "gradle"; 196 } 197 198 String fixedName = replaceRegexWithPrefix(entryName, ".*/src/main/(.*)", "app/src/main/"); 199 200 if (fixedName == null) { 201 fixedName = replaceRegexWithPrefix(entryName, ".*/main/(.*)", "app/src/main/"); 202 } 203 204 if (fixedName == null) { 205 fixedName = replaceRegexWithPrefix(entryName, ".*/java/(.*)", "app/src/main/java/"); 206 } 207 208 if (fixedName == null) { 209 fixedName = replaceRegexWithPrefix(entryName, "^edu/(.*)", "app/src/main/java/edu/"); 210 } 211 212 if (fixedName == null) { 213 fixedName = replaceRegexWithPrefix(entryName, ".*/gradle/(.*)", "gradle/"); 214 } 215 216 return fixedName; 217 } 218 219 private static String replaceRegexWithPrefix(String entryName, String regex, String replaceWithPrefix) { 220 Pattern pattern = Pattern.compile(regex); 221 Matcher matcher = pattern.matcher(entryName); 222 223 if (matcher.matches()) { 224 String portionUnderMain = matcher.group(1); 225 if ("".equals(portionUnderMain)) { 226 return null; 227 228 } else { 229 return replaceWithPrefix + portionUnderMain; 230 } 231 232 } else { 233 return null; 234 } 235 } 236 237 private static File getFixedZipFile(File zipFile, File outputDirectory) { 238 return new File(outputDirectory, zipFile.getName()); 239 } 240 241 private static void usage(String message) { 242 PrintStream err = System.err; 243 244 err.println("+++ " + message); 245 err.println(); 246 err.println("usage: java GwtZipFixer gradeBook outputDirectory zipFile+"); 247 err.println(" gradeBook Grade book XML file"); 248 err.println(" outputDirectory Name of direct into which fixed zip files should be written"); 249 err.println(" zipFile Name of zip file to be fixed"); 250 err.println(); 251 err.println("Fixes the contents of a zip file submitted for the GWT "); 252 err.println("project so that it will work with the grading scripts"); 253 err.println(); 254 255 System.exit(1); 256 } 257}