/** * Copyright (C) 2013-2014 Olaf Lessenich * Copyright (C) 2014-2015 University of Passau, Germany * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA * * Contributors: * Olaf Lessenich <lessenic@fim.uni-passau.de> * Georg Seibt <seibt@fim.uni-passau.de> */ package de.fosd.jdime; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.security.Permission; import java.util.Arrays; import java.util.Date; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.IntStream; import java.util.stream.Stream; import de.fosd.jdime.artifact.Artifact; import de.fosd.jdime.artifact.ArtifactList; import de.fosd.jdime.artifact.ast.ASTNodeArtifact; import de.fosd.jdime.artifact.file.FileArtifact; import de.fosd.jdime.config.JDimeConfig; import de.fosd.jdime.config.merge.MergeContext; import de.fosd.jdime.config.merge.MergeScenario; import de.fosd.jdime.config.merge.MergeType; import de.fosd.jdime.execption.AbortException; import de.fosd.jdime.operations.MergeOperation; import de.fosd.jdime.stats.KeyEnums; import de.fosd.jdime.stats.Statistics; import de.fosd.jdime.strategy.MergeStrategy; import de.fosd.jdime.strategy.StrategyNotFoundException; import de.fosd.jdime.strdump.DumpMode; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.ParseException; import org.apache.commons.io.FileUtils; import static de.fosd.jdime.config.CommandLineConfigSource.CLI_DUMP; import static de.fosd.jdime.config.CommandLineConfigSource.CLI_HELP; import static de.fosd.jdime.config.CommandLineConfigSource.CLI_INSPECT_ELEMENT; import static de.fosd.jdime.config.CommandLineConfigSource.CLI_INSPECT_METHOD; import static de.fosd.jdime.config.CommandLineConfigSource.CLI_MODE; import static de.fosd.jdime.config.CommandLineConfigSource.CLI_VERSION; import static de.fosd.jdime.config.JDimeConfig.*; /** * Contains the main method of the application. */ public final class Main { private static final Logger LOG = Logger.getLogger(Main.class.getCanonicalName()); public static final String TOOLNAME = "jdime"; public static final String VERSION = "0.4.1"; private static final String MODE_LIST = "list"; private static final int EXIT_ABORTED = 2; private static final int EXIT_FAILURE = 1; private static JDimeConfig config; /** * Prevent instantiation. */ private Main() {} /** * Perform a merge operation on the input files or directories. * * @param args * command line arguments */ public static void main(String[] args) { try { run(args); } catch (AbortException e) { LOG.log(Level.SEVERE, e.getCause(), () -> "Aborting the merge."); System.exit(EXIT_ABORTED); } catch (Throwable e) { LOG.log(Level.SEVERE, e, () -> "Uncaught exception."); System.exit(EXIT_FAILURE); } } /** * Perform a merge operation on the input files or directories. * * @param args * command line arguments */ public static void run(String[] args) throws IOException, ParseException, InterruptedException { MergeContext context = new MergeContext(); if (!parseCommandLineArgs(context, args)) { return; } ArtifactList<FileArtifact> inputFiles = context.getInputFiles(); FileArtifact output = context.getOutputFile(); for (FileArtifact inputFile : inputFiles) { if (inputFile.isDirectory() && !context.isRecursive()) { String msg = "To merge directories, the argument '-r' has to be supplied. See '-help' for more information!"; LOG.severe(msg); System.err.println(msg); return; } } if (output != null && output.exists() && !output.isEmpty()) { boolean overwrite; try (BufferedReader r = new BufferedReader(new InputStreamReader(System.in))) { System.err.println("Output directory is not empty!"); System.err.println("Delete '" + output.getFullPath() + "'? [y/N]"); String response = r.readLine().trim().toLowerCase(); overwrite = response.length() != 0 && response.charAt(0) == 'y'; } if (overwrite) { LOG.warning("File exists and will be overwritten."); output.remove(); if (output.isDirectory()) { FileUtils.forceMkdir(output.getFile()); } } else { String msg = "File exists and will not be overwritten."; LOG.severe(msg); System.err.println(msg); return; } } if (context.isInspect()) { inspectElement(context.getInputFiles().get(0), context.getInspectArtifact(), context.getInspectionScope()); } else if (context.getDumpMode() != DumpMode.NONE) { for (FileArtifact artifact : context.getInputFiles()) { dump(artifact, context.getDumpMode()); } } else { if (context.getInputFiles().size() < MergeType.MINFILES) { printCLIHelp(); return; } merge(context); if (context.hasStatistics()) { outputStatistics(context.getStatistics()); } } if (LOG.isLoggable(Level.CONFIG)) { Map<MergeScenario<?>, Throwable> crashes = context.getCrashes(); String ls = System.lineSeparator(); StringBuilder sb = new StringBuilder(); sb.append(String.format("%d crashes occurred while merging:%n", crashes.size())); for (Map.Entry<MergeScenario<?>, Throwable> entry : crashes.entrySet()) { sb.append("* ").append(entry.getValue().toString()).append(ls); sb.append(" ").append(entry.getKey().toString().replace(" ", ls + " ")).append(ls); } LOG.config(sb.toString()); } } /** * Outputs the given <code>Statistics</code> according to the set configuration options. * * @param statistics * the <code>Statistics</code> to output */ private static void outputStatistics(Statistics statistics) { String hrOut = config.get(STATISTICS_HR_OUTPUT).orElse(STATISTICS_OUTPUT_STDOUT); String xmlOut = config.get(STATISTICS_XML_OUTPUT).orElse(STATISTICS_OUTPUT_OFF); switch (hrOut) { case STATISTICS_OUTPUT_OFF: LOG.fine("Human readable statistics output is disabled."); break; case STATISTICS_OUTPUT_STDOUT: statistics.print(System.out); break; default: { File f = new File(hrOut); if (f.isDirectory()) { String name = config.get(STATISTICS_HR_NAME).orElse(STATISTICS_HR_DEFAULT_NAME); f = new File(f, String.format(name, new Date())); } if (config.getBoolean(STATISTICS_OUTPUT_USE_UNIQUE_FILES).orElse(true)) { f = findNonExistent(f); } try { statistics.print(f); } catch (FileNotFoundException e) { LOG.log(Level.WARNING, e, () -> "Statistics output failed."); } } } switch (xmlOut) { case STATISTICS_OUTPUT_OFF: LOG.fine("XML statistics output is disabled."); break; case STATISTICS_OUTPUT_STDOUT: statistics.printXML(System.out); System.out.println(); break; default: { File f = new File(xmlOut); if (f.isDirectory()) { String name = config.get(STATISTICS_XML_NAME).orElse(STATISTICS_XML_DEFAULT_NAME); f = new File(f, String.format(name, new Date())); } if (config.getBoolean(STATISTICS_OUTPUT_USE_UNIQUE_FILES).orElse(true)) { f = findNonExistent(f); } try { statistics.printXML(f); } catch (FileNotFoundException e) { LOG.log(Level.WARNING, e, () -> "Statistics output failed."); } } } } /** * Returns a <code>File</code> (possibly <code>f</code>) that does not exist in the parent directory of * <code>f</code>. If <code>f</code> exists an increasing number is appended to the name of <code>f</code> until * a <code>File</code> is found that does not exist. * * @param f * the <code>File</code> to find a non existent version of * @return a <code>File</code> in the parent directory of <code>f</code> that does not exist */ private static File findNonExistent(File f) { if (!f.exists()) { return f; } String fullName = f.getName(); String name; String extension; int pos = fullName.lastIndexOf('.'); if (pos != -1) { name = fullName.substring(0, pos); extension = fullName.substring(pos, fullName.length()); } else { name = fullName; extension = ""; } File parent = f.getParentFile(); Stream<File> files = IntStream.range(0, Integer.MAX_VALUE).mapToObj(v -> { String fileName = String.format("%s_%d%s", name, v, extension); return new File(parent, fileName); }); File nextFree = files.filter(file -> !file.exists()).findFirst().orElseThrow(() -> new RuntimeException("Can not find a file that does not exist.")); return nextFree; } /** * Parses command line arguments and initializes program. * * @param context * merge context * @param args * command line arguments * @return true if program should continue */ private static boolean parseCommandLineArgs(MergeContext context, String[] args) { JDimeConfig config; try { config = new JDimeConfig(args); } catch (ParseException e) { System.err.println("Failed to parse the command line arguments " + Arrays.toString(args)); System.err.println(e.getMessage()); System.exit(EXIT_FAILURE); return false; } Main.config = config; if (config.getBoolean(CLI_HELP).orElse(false)) { printCLIHelp(); return false; } if (config.getBoolean(CLI_VERSION).orElse(false)) { Optional<String> commit = config.get(JDIME_COMMIT); if (commit.isPresent()) { System.out.printf("%s version %s commit %s%n", TOOLNAME, VERSION, commit.get()); } else { System.out.printf("%s version %s%n", TOOLNAME, VERSION); } return false; } Function<String, Optional<DumpMode>> dmpModeParser = mode -> { try { return Optional.of(DumpMode.valueOf(mode.toUpperCase())); } catch (IllegalArgumentException e) { LOG.log(Level.WARNING, e, () -> "Invalid dump format " + mode); return Optional.of(DumpMode.NONE); } }; Optional<Integer> inspectElement = config.getInteger(CLI_INSPECT_ELEMENT); KeyEnums.Type scope = null; if (inspectElement.isPresent()) { scope = KeyEnums.Type.NODE; } else if ((inspectElement = config.getInteger(CLI_INSPECT_METHOD)).isPresent()) { scope = KeyEnums.Type.METHOD; } context.setInspectArtifact(inspectElement.orElse(0)); context.setInspectionScope(scope); context.setDumpMode(config.get(CLI_DUMP, dmpModeParser).orElse(DumpMode.NONE)); Optional<String> mode = config.get(CLI_MODE).map(String::toLowerCase); if (mode.isPresent()) { if (MODE_LIST.equals(mode.get())) { printStrategies(); return false; } else { try { context.setMergeStrategy(MergeStrategy.parse(mode.get())); } catch (StrategyNotFoundException e) { LOG.log(Level.SEVERE, e, () -> "Strategy not found."); return false; } } } context.configureFrom(config); return true; } /** * Prints the available strategies. */ private static void printStrategies() { System.out.println("Available merge strategies:"); for (String s : MergeStrategy.listStrategies()) { System.out.println("\t- " + s); } } /** * Merges the input files. * * @param context * merge context * @throws InterruptedException * If a thread is interrupted * @throws IOException * If an input output exception occurs */ public static void merge(MergeContext context) throws IOException, InterruptedException { ArtifactList<FileArtifact> inFiles = context.getInputFiles(); FileArtifact outFile = context.getOutputFile(); if (context.isFilterInputDirectories()) { inFiles.forEach(FileArtifact::filterNonJavaFiles); } boolean conditional = context.isConditionalMerge(); MergeOperation<FileArtifact> merge = new MergeOperation<>(inFiles, outFile, null, null, conditional); merge.apply(context); } /** * Dumps the given <code>FileArtifact</code> using the <code>mode</code>. * * @param artifact * the <code>Artifact</code> to dump * @param mode * the dump format */ private static void dump(FileArtifact artifact, DumpMode mode) { if (mode == DumpMode.NONE) { return; } if (mode == DumpMode.FILE_DUMP || artifact.isDirectory()) { System.out.println(artifact.dump(mode)); } else { SecurityManager prevSecManager = System.getSecurityManager(); SecurityManager noExitManager = new SecurityManager() { @Override public void checkPermission(Permission perm) { // allow anything. } @Override public void checkPermission(Permission perm, Object context) { // allow anything. } @Override public void checkExit(int status) { super.checkExit(status); throw new SecurityException("Captured attempt to exit JVM."); } }; ASTNodeArtifact astArtifact; System.setSecurityManager(noExitManager); try { astArtifact = new ASTNodeArtifact(artifact); } catch (RuntimeException e) { LOG.log(Level.WARNING, e, () -> "Could not parse " + artifact + " to an ASTNodeArtifact."); return; } finally { System.setSecurityManager(prevSecManager); } System.out.println(astArtifact.dump(mode)); } } /** * Parses the given <code>artifact</code> to an AST and attempts to find a node with the given <code>number</code> * in the tree. If found, the {@link DumpMode#PRETTY_PRINT_DUMP} will be used to dump the node to standard out. * If <code>scope</code> is not {@link KeyEnums.Type#NODE}, the method will walk up the tree to find a node that * fits the requested <code>scope</code> and dump it instead. * * @param artifact * the <code>FileArtifact</code> to parse to an AST * @param number * the number of the <code>artifact</code> in the AST to find * @param scope * the scope to dump */ private static void inspectElement(FileArtifact artifact, int number, KeyEnums.Type scope) { ASTNodeArtifact astArtifact = new ASTNodeArtifact(artifact); Optional<Artifact<ASTNodeArtifact>> foundNode = astArtifact.find(number); if (foundNode.isPresent()) { Artifact<ASTNodeArtifact> element = foundNode.get(); if (scope != KeyEnums.Type.NODE) { // walk tree upwards until scope fits while (scope != element.getType() && !element.isRoot()) { element = element.getParent(); } } System.out.println(element.dump(DumpMode.PRETTY_PRINT_DUMP)); } else { LOG.log(Level.WARNING, () -> "Could not find a node with number " + number + "."); } } /** * Prints usage information and a help text about the command line options to <code>System.out</code>. */ private static void printCLIHelp() { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp(Main.TOOLNAME, config.getCmdLine().getOptions(), true); } }