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}