/* * #%L * Nazgul Project: nazgul-core-launcher-api * %% * Copyright (C) 2010 - 2017 jGuru Europe AB * %% * Licensed under the jGuru Europe AB license (the "License"), based * on Apache License, Version 2.0; you may not use this file except * in compliance with the License. * * You may obtain a copy of the License at * * http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% * */ package se.jguru.nazgul.core.algorithms.launcher.api; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.jguru.nazgul.core.algorithms.api.Validate; import javax.validation.constraints.NotNull; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; /** * Abstract StandardLifecycle implementation, handling command-line arguments, exception printouts and help screens. * * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB */ @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public abstract class AbstractApplicationLauncher implements StandardLifecycle { // Our logger private static final Logger log = LoggerFactory.getLogger(AbstractApplicationLauncher.class); // Constants private static final String NAZGUL_PACKAGES_REGEXP = "^se\\.jguru\\.nazgul\\..*"; private static final String ARG_VERBOSE = "verbose"; private static final String ARG_HELP = "help"; // Internal state private List<Pattern> verboseLoggerPatternList; private Options options; private CommandLine commandLine; private boolean dontExecuteApplication = false; private String briefApplicationDescription; private String briefCommandLineSyntax; private URL jarfileLocation; private String appVersion; /** * Compound constructor, creating an AbstractApplication instance from the given arguments. * * @param briefApplicationDescription A brief, human-readable description of the application. * <strong>Example:</strong> <code>Performs load tests on * the CGW-internal radius server.</code> * @param commandLineArguments The command-line arguments supplied to a main method call. * @param appVersion A version descriptor for this application. */ protected AbstractApplicationLauncher(@NotNull final String briefApplicationDescription, @NotNull final String[] commandLineArguments, @NotNull final String appVersion) { this(briefApplicationDescription, commandLineArguments, appVersion, Collections.singletonList(NAZGUL_PACKAGES_REGEXP)); } /** * Compound constructor, creating an AbstractApplication instance from the given arguments. * * @param briefApplicationDescription A brief, human-readable description of the application. * <strong>Example:</strong> <code>Performs load tests on * the CGW-internal radius server.</code> * @param commandLineArguments The command-line arguments supplied to a main method call. * @param appVersion A version descriptor for this application. * @param patternsForVerboseLogging A List holding java regexp patterns, defining which */ protected AbstractApplicationLauncher(@NotNull final String briefApplicationDescription, @NotNull final String[] commandLineArguments, @NotNull final String appVersion, @NotNull final List<String> patternsForVerboseLogging) { // Check sanity Validate.notEmpty(briefApplicationDescription, "briefApplicationDescription"); Validate.notNull(commandLineArguments, "commandLineArguments"); Validate.notEmpty(appVersion, "appVersion"); Validate.notEmpty(patternsForVerboseLogging, "patternsForVerboseLogging"); // Create internal state this.appVersion = appVersion; this.briefApplicationDescription = briefApplicationDescription; jarfileLocation = getClass().getProtectionDomain().getCodeSource().getLocation(); this.briefCommandLineSyntax = "java -jar " + getJarFile().getName() + " [arguments]"; // Define the standard options options = new Options(); options.addOption(ARG_VERBOSE, "verbose", false, "Optional. If provided, use verbose logging."); options.addOption(ARG_HELP, "help", false, "Optional. Prints the application brief help text."); addOptions(options); // Create a pattern list for logger verbosity matching verboseLoggerPatternList = new ArrayList<Pattern>(); for (String current : patternsForVerboseLogging) { verboseLoggerPatternList.add(Pattern.compile(current)); } if (log.isDebugEnabled()) { log.debug("Got jarfileLocation: " + getJarPath()); } // Create the command line final CommandLineParser parser = new DefaultParser(); try { // Parse the command line. commandLine = parser.parse(options, commandLineArguments); adjustLoggingLevel(commandLine); if (commandLine.hasOption(ARG_HELP)) { dontExecuteApplication = true; printUsage("Help printed."); } } catch (final ParseException e) { // Whoops dontExecuteApplication = true; printUsage(e.getMessage()); } } /** * @return The File of the executable JAR file where this class executes, * or {@code null} if this class executes outside of a JAR file. */ protected final File getJarFile() { return new File(jarfileLocation.getPath()); } /** * @return The path of the executing JarFile. */ protected final String getJarPath() { try { return getJarFile().getCanonicalPath(); } catch (IOException e) { return getJarFile().getAbsolutePath(); } } /** * Overload this method to add any Command-line Options to be supported by this AbstractApplication. * * @param options The command-line options to be used by this AbstractApplication instance. */ protected void addOptions(final Options options) { // By default, adds nothing. } /** * Main entrypoint to the application, which executes its lifecycle by * invoking the following methods (in order): * <pre> * try { * // First, validate the given CLI arguments. * validateArguments(commandLine); * * // Second, fire the actual application. * runApplication(commandLine); * } catch (RuntimeException e) { * * // Whoops. * printUsage(e.getMessage()); * } * </pre> */ public final void execute() { if (!dontExecuteApplication) { try { // First, validate the given CLI arguments. validateArguments(commandLine); // Second, fire the actual application. runApplication(); } catch (RuntimeException e) { StringBuilder messageBuilder = new StringBuilder(); messageBuilder.append(e.getMessage()).append("\n"); final Throwable cause = e.getCause(); if (cause != null) { if (cause.getMessage() != null) { messageBuilder.append(cause.getMessage()).append("\n"); } else { messageBuilder.append(" ... type: ").append(cause.getClass().getSimpleName()).append("\n"); } } // Whoops. printUsage(messageBuilder.toString()); } } } /** * Checks if the provided key is given as an argument on the command line or not. * * @param argument The argument which should be validated. * @return {@code true} if the provided argument was supplied on the command line launching this application. */ protected final boolean isArgumentSupplied(final String argument) { try { return commandLine.hasOption(argument); } catch (Exception e) { return false; } } /** * Acquires the value of a command-line argument, or {@code fallbackValue} should * the argument not have been supplied. * * @param argument the argument whose value should be retrieved. * @param fallbackValue a value returned if the given argument had no value or was not supplied * on the command line launching this application. * @return the value of a command-line argument, or {@code fallbackValue} should * the argument not have been supplied. */ protected final String getValue(final String argument, final String fallbackValue) { return isArgumentSupplied(argument) ? commandLine.getOptionValue(argument) : fallbackValue; } /** * Retrieves the application description of this AbstractApplicationLauncher instance. * * @return the application description of this AbstractApplicationLauncher instance. */ public final String getBriefApplicationDescription() { return briefApplicationDescription; } // // Private helpers // private void adjustLoggingLevel(final CommandLine commandLine) { // Verbose logging? if (commandLine.hasOption(ARG_VERBOSE)) { // Lower the logging squelch for all relevant Appenders to DEBUG final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); for (ch.qos.logback.classic.Logger current : context.getLoggerList()) { final String name = current.getName(); for (Pattern currentPattern : verboseLoggerPatternList) { if (currentPattern.matcher(name).matches()) { current.setLevel(Level.DEBUG); } } } } } /** * {@inheritDoc} */ public void printUsage(final String errorMessage) { StringWriter helpText = new StringWriter(); helpText.append("\n\n---------------------------------------\n"); HelpFormatter formatter = new HelpFormatter(); try { formatter.printHelp(new PrintWriter(helpText), 80, briefCommandLineSyntax, getBriefApplicationDescription() + "\n\nArguments:", options, 2, 2, "\nMessage: " + errorMessage); } catch (Exception e) { helpText.append("\nMessage: " + errorMessage + "\n"); } helpText.append("\n\n-- Version: " + appVersion); helpText.append("\n\n---------------------------------------"); // Write using the logger. log.info(helpText.toString()); } }