/* Copyright (c) 2008-2010, developers of the Ascension Log Visualizer * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom * the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package com.googlecode.logVisualizer.parser; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Comparator; import java.util.List; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.java.dev.spellcast.utilities.UtilityConstants; import com.googlecode.logVisualizer.Settings; import com.googlecode.logVisualizer.util.DataNumberPair; import com.googlecode.logVisualizer.util.textualLogs.TextLogCreator; import com.googlecode.logVisualizer.util.textualLogs.TextLogCreator.TextualLogVersion; /** * This class gives access to methods to create condensed mafia ascension logs * or parsed ascension logs. * <p> * Note that all methods of this class are thread-safe as long as not the same * files are given to different threads or the created files by the given method * have the same file name and are stored in the same directory. */ public final class LogsCreator { // This class is not to be instanced. private LogsCreator() { } /** * Creates and returns condensed mafia logs which hold single ascensions * from start to end in a single file. * <p> * Day changes (the junction between two normal log files of a single * ascension) will be separated by the string {@code ===Day _dayNumber_===}, * which is in essence the same as the one used in parsed ascension logs to * show day changes. Note that if the were multiple days on which no login * occurred, it will be catched by the parsing mechanism behind this method, * resulting in the right number of day change strings right after each * other. * <p> * There are sometimes cases of KolMafia stating that it is the same * real-life date, while the KoL date changed (this strongly depends on the * users time zone). If such a case is recognised, the line * "Day change occurred" will be added to the player snapshot in which this * date change was noticed. * <p> * The number of files depends on the number of ascensions present inside * the given mafia logs. * <p> * Please note that the condensed mafia logs created by this method are * stored in the directory for temporal data as denoted by * {@link UtilityConstants#TEMP_LOCATION}. It is the <b>responsibility of * the programmer using this method to delete these files as soon as they * are not needed anymore</b> to ensure that no bugs appear because of * leftover files and no size bloat of the Ascension Log Visualizer * directory happens. * * @param mafiaLogs * The mafia logs which should be condensed into mafia logs which * each holds a single ascension. * @return The condensed mafia logs. * @throws IOException * if there was a problem while accessing the given mafia logs * or writing the condensed ones * @throws NullPointerException * if mafiaLogs is {@code null} * @throws IllegalArgumentException * if mafiaLogs does not contain any elements */ public static final File[] createCondensedMafiaLogs(final File[] mafiaLogs) throws IOException { return new CondensedMafiaLogsCreator(mafiaLogs).parse(); } /** * Creates and saves parsed ascension logs. The format of those logs is * similar to the one used by the AFH MafiaLog Parser. ( * {@link TextLogCreator} handles the log format) * <p> * The file names of the created logs have the format * {@code USERNAME_ascendYYYYMMDD.txt}, where Y is the year, M is the month * and D is the day of the first day of that ascension. * * @param mafiaLogs * The mafia logs which should be turned into parsed ascension * logs. * @param savingDestDir * The directory inside which the parsed ascension logs should be * saved in. * @return A DataNumberPair list containing pairs with filenames and turn * numbers of logs files that were attempted to be created, but had * an exception thrown during the parsing process. The turn number * denotes the turn after which the exception occurred. This list * will be empty if all files were correctly parsed. * @throws IOException * if there was a problem while accessing or writing files * handled by this method * @throws NullPointerException * if mafiaLogs is {@code null}; if savingDestDir is * {@code null} * @throws IllegalArgumentException * if mafiaLogs does not contain any elements; if the directory * savingDestDir does not exist; if savingDestDir is not a * directory */ public static final List<DataNumberPair<String>> createParsedLogs( final File[] mafiaLogs, final File savingDestDir, final TextualLogVersion logVersion) throws IOException { if (!savingDestDir.exists()) { throw new IllegalArgumentException("The directory doesn't exist."); } if (!savingDestDir.isDirectory()) { throw new IllegalArgumentException( "The given file is not a directory."); } final List<DataNumberPair<String>> errorFileList = new ArrayList<>(); final File[] condensedMafiaLogs = LogsCreator .createCondensedMafiaLogs(mafiaLogs); // 4 Threads should be a high enough number to not slow the computation // too much down by scheduler overhead while still making use of // threaded computing. final ExecutorService executor = Executors.newFixedThreadPool(Runtime .getRuntime().availableProcessors() * 4); for (final File f : condensedMafiaLogs) { executor.execute(new Runnable() { @Override public void run() { final MafiaLogParser parser = new MafiaLogParser( f, Settings.getSettingBoolean("Include mafia log notes")); try { parser.parse(); final File parsedLog = new File( savingDestDir, LogsCreator .getParsedLogNameFromCondensedMafiaLog(f .getName())); if (parsedLog.exists()) { parsedLog.delete(); } parsedLog.createNewFile(); TextLogCreator.saveTextualLogToFile( parser.getLogData(), parsedLog, logVersion); } catch (final IOException e) { // Add the erroneous log to the error file list. errorFileList.add(DataNumberPair.of(LogsCreator .getParsedLogNameFromCondensedMafiaLog(f .getName()), parser.getLogData() .getTurnsSpent().last().getEndTurn())); // Print stack trace and the file name of the file in // which the error happened. System.err.println(f.getName()); e.printStackTrace(); } } }); } // Wait for all threads to finish. executor.shutdown(); try { executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (final InterruptedException e) { e.printStackTrace(); } // Temporary files should be deleted after use. Possible subdirectories // are ignored here. for (final File f : UtilityConstants.TEMP_LOCATION.listFiles()) { if (!f.isDirectory()) { f.delete(); } } return errorFileList; } /** * Takes the file name of a condensed mafia log and changes it into the * proper format for parsed ascension logs. * <p> * File names of condensed mafia logs use the format * {@code USERNAME-YYYYMMDD.txt}, where Y is the year, M is the month and D * is the day of the first day of that ascension. * * @param condensedMafiaLogFileName * File name of the condensed mafia log. * @return The proper parsed ascension log file name. */ public static String getParsedLogNameFromCondensedMafiaLog( final String condensedMafiaLogFileName) { final String userName = condensedMafiaLogFileName.substring(0, condensedMafiaLogFileName.lastIndexOf("-")); return userName.concat(condensedMafiaLogFileName.substring( userName.length()).replace("-", "_ascend")); } /** * A helper class to condense mafia logs into holding a single ascension per * file. */ private static final class CondensedMafiaLogsCreator { private static final Pattern NOT_USER_NAME_PATTERN = Pattern .compile("_\\d+\\.txt"); private static final Pattern ASCENDED_PATTERN = Pattern .compile("ascend\\.php\\?action=ascend.*confirm=on.*confirm2=on.*"); private static final List<String> months = new ArrayList<>( Arrays.asList("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")); private static final FilenameFilter CONDENSED_MAFIA_LOG_FILTER = new FilenameFilter() { private final Matcher mafiaLogMatcher = Pattern.compile( ".*\\-\\d+\\.txt$").matcher(""); @Override public boolean accept(final File dir, final String name) { return this.mafiaLogMatcher.reset(name).matches(); } }; private final Matcher ascendedMatcher = CondensedMafiaLogsCreator.ASCENDED_PATTERN .matcher(""); private final File[] mafiaLogs; private PrintWriter currentWritingFile; /** * @param mafiaLogs * The mafia logs which should be turned into parsed * ascension logs. * @throws NullPointerException * if mafiaLogs is {@code null} * @throws IllegalArgumentException * if mafiaLogs does not contain any elements */ CondensedMafiaLogsCreator(final File[] mafiaLogs) { if (mafiaLogs == null) { throw new NullPointerException( "The File array mafiaLogs must not be null."); } if (mafiaLogs.length == 0) { throw new IllegalArgumentException( "The File array mafiaLogs must not be empty."); } // Sort array in case it isn't already in the proper order, which is // oldest mafia log first. Arrays.sort(mafiaLogs, new Comparator<File>() { @Override public int compare(final File o1, final File o2) { return o1.getName().compareToIgnoreCase(o2.getName()); } }); this.mafiaLogs = mafiaLogs; } /** * Creates and returns condensed mafia logs which hold single ascensions * from start to end in a single file. * <p> * Day changes (the junction between two normal log files of a single * ascension) will be separated by the string {@code ===Day * _dayNumber_===}, which is in essence the same as the one used in * parsed ascension logs to show day changes. Note that if the were * multiple days on which no login occurred, it will be catched by the * parsing mechanism behind this method, resulting in the right number * of day change strings right after each other. * <p> * There are sometimes cases of KolMafia stating that it is the same * real-life date, while the KoL date changed (this strongly depends on * the users time zone). If such a case is recognised, the line * "Day change occurred" will be added to the player snapshot in which * this date change was noticed. * <p> * Please note that the condensed mafia logs created by this method are * stored in the directory for temporal data as denoted by * {@link UtilityConstants#TEMP_LOCATION}. These files should be deleted * after use. * * @return The condensed mafia logs. * @throws IOException * if there was a problem while accessing the given mafia * logs or writing the condensed ones */ File[] parse() throws IOException { String userName = this.mafiaLogs[0].getName() .substring(0, this.mafiaLogs[0].getName().lastIndexOf("_")) .toLowerCase(); String lastKolDate = null; int dayNumber = 1; Calendar lastLogDate = UsefulPatterns .getMafiaLogCalendarDate(this.mafiaLogs[0]); this.openNextWritingFile(this.mafiaLogs[0].getName()); for (final File f : this.mafiaLogs) { final String currentLogUserName = f.getName() .substring(0, f.getName().lastIndexOf("_")) .toLowerCase(); if (!userName.equals(currentLogUserName)) { this.openNextWritingFile(f.getName()); dayNumber = 1; lastLogDate = UsefulPatterns.getMafiaLogCalendarDate(f); userName = currentLogUserName; lastKolDate = null; } try { final Calendar currentLogDate = UsefulPatterns .getMafiaLogCalendarDate(f); long dayDiff = (currentLogDate.getTimeInMillis() - lastLogDate .getTimeInMillis()) / 86400000; while (dayDiff > 0) { dayDiff--; dayNumber++; lastKolDate = null; this.currentWritingFile.println(); this.currentWritingFile.println("===Day " + dayNumber + "==="); this.currentWritingFile.println(); } lastLogDate = currentLogDate; try (final BufferedReader br = new BufferedReader( new FileReader(f))) { String tmpLine; while ((tmpLine = br.readLine()) != null) { this.currentWritingFile.println(tmpLine); for (final String s : CondensedMafiaLogsCreator.months) { if (tmpLine.startsWith(s) && !tmpLine .startsWith("April Fool's Day")) { final String currentKolDate = tmpLine .substring(tmpLine.lastIndexOf("-") + 2); if (lastKolDate == null) { lastKolDate = currentKolDate; } else if (!currentKolDate .equals(lastKolDate)) { this.currentWritingFile .println("Day change occurred"); dayNumber++; lastLogDate.add(Calendar.DAY_OF_MONTH, 1); lastKolDate = currentKolDate; } } } if (this.ascendedMatcher.reset(tmpLine).matches()) { this.openNextWritingFile(f.getName()); dayNumber = 1; } } br.close(); } } catch (final IOException e) { e.printStackTrace(); } } // Close print-stream after the last log was read. if (this.currentWritingFile != null) { this.currentWritingFile.close(); } final File[] condensedMafiaLogs = UtilityConstants.TEMP_LOCATION .listFiles(CondensedMafiaLogsCreator.CONDENSED_MAFIA_LOG_FILTER); // Sort array in case it isn't already in the proper order, which is // oldest mafia log first. Arrays.sort(condensedMafiaLogs, new Comparator<File>() { @Override public int compare(final File o1, final File o2) { return o1.getName().compareToIgnoreCase(o2.getName()); } }); return condensedMafiaLogs; } /** * Closes the current PrintWriter if one is present starts a new * condensed mafia log with a file name based on the current mafia log. * <p> * The file name will use the format {@code USERNAME-YYYYMMDD.txt}, * where Y is the year, M is the month and D is the day of the current * mafia log, which also is the start date of the ascension represented * be the condensed mafia log. * * @param currentMafiaLogFileName * The file name of the current mafia log. */ private void openNextWritingFile(final String currentMafiaLogFileName) throws IOException { if (this.currentWritingFile != null) { this.currentWritingFile.close(); } try (final Scanner scanner = new Scanner(currentMafiaLogFileName)) { scanner.useDelimiter(CondensedMafiaLogsCreator.NOT_USER_NAME_PATTERN); final String fileName = scanner.next().replace("_", " ") + "-" + UsefulPatterns.getLogDate(currentMafiaLogFileName) + ".txt"; scanner.close(); this.currentWritingFile = new PrintWriter( new File(UtilityConstants.TEMP_LOCATION, fileName) .getAbsolutePath()); } } } }