/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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. */ package com.google.template.soy; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.template.soy.SoyFileSet.Builder; import com.google.template.soy.base.internal.SoyFileKind; import com.google.template.soy.error.SoyCompilationException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.OptionDef; import org.kohsuke.args4j.spi.OptionHandler; import org.kohsuke.args4j.spi.Parameters; import org.kohsuke.args4j.spi.Setter; /** * Utilities for classes with a {@code main()} method. * */ final class MainClassUtils { /** * Represents a top-level entry point into the Soy codebase. Used by {@link #run} to catch * unexpected exceptions and print errors. */ interface Main { void main() throws IOException, SoyCompilationException; } private MainClassUtils() {} // NOTE: all the OptionHandler types need to be public with public constructors so args4j can use // them. /** * OptionHandler for args4j that handles a boolean. * * <p>The difference between this handler and the default boolean option handler supplied by * args4j is that the default one doesn't take any param, so can only be used to turn on boolean * flags, but never to turn them off. This implementation allows an optional param value * true/false/1/0 so that the user can turn on or off the flag. */ public static final class BooleanOptionHandler extends OptionHandler<Boolean> { /** {@link OptionHandler#OptionHandler(CmdLineParser,OptionDef,Setter)} */ public BooleanOptionHandler( CmdLineParser parser, OptionDef option, Setter<? super Boolean> setter) { super(parser, option, setter); } @Override public int parseArguments(Parameters params) throws CmdLineException { boolean value; boolean hasParam; try { String nextArg = params.getParameter(0); if (nextArg.equalsIgnoreCase("true") || nextArg.equals("1")) { value = true; hasParam = true; } else if (nextArg.equalsIgnoreCase("false") || nextArg.equals("0")) { value = false; hasParam = true; } else { // Next arg is not a param for this flag. No param means set flag to true. value = true; hasParam = false; } } catch (CmdLineException e) { // No additional args on command line. No param means set flag to true. value = true; hasParam = false; } setter.addValue(value); return hasParam ? 1 : 0; } @Override public String getDefaultMetaVariable() { return null; } } /** OptionHandler for args4j that handles a comma-delimited list. */ abstract static class ListOptionHandler<T> extends OptionHandler<T> { /** {@link OptionHandler#OptionHandler(CmdLineParser,OptionDef,Setter)} */ ListOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super T> setter) { super(parser, option, setter); } /** * Parses one item from the list into the appropriate type. * * @param item One item from the list. * @return The object representation of the item. */ abstract T parseItem(String item); @Override public int parseArguments(Parameters params) throws CmdLineException { String parameter = params.getParameter(0); // An empty string should be an empty list, not a list containing the empty item if (!parameter.isEmpty()) { for (String item : parameter.split(",")) { setter.addValue(parseItem(item)); } } return 1; } @Override public String getDefaultMetaVariable() { return "ITEM,ITEM,..."; } } /** OptionHandler for args4j that handles a comma-delimited list of strings. */ public static final class StringListOptionHandler extends ListOptionHandler<String> { /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */ public StringListOptionHandler( CmdLineParser parser, OptionDef option, Setter<? super String> setter) { super(parser, option, setter); } @Override String parseItem(String item) { return item; } } /** OptionHandler for args4j that handles a comma-delimited list of guice modules. */ public static final class ModuleListOptionHandler extends ListOptionHandler<Module> { /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */ public ModuleListOptionHandler( CmdLineParser parser, OptionDef option, Setter<? super Module> setter) { super(parser, option, setter); } @Override Module parseItem(String item) { return instantiatePluginModule(item); } } /** OptionHandler for args4j that handles a comma-delimited list of files. */ public static final class FileListOptionHandler extends ListOptionHandler<File> { /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */ public FileListOptionHandler( CmdLineParser parser, OptionDef option, Setter<? super File> setter) { super(parser, option, setter); } @Override File parseItem(String item) { return new File(item); } } /** OptionHandler for args4j that handles a comma-delimited list of strings. */ public static final class ModuleOptionHandler extends OptionHandler<Module> { /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */ public ModuleOptionHandler( CmdLineParser parser, OptionDef option, Setter<? super Module> setter) { super(parser, option, setter); } @Override public int parseArguments(Parameters params) throws CmdLineException { String parameter = params.getParameter(0); // An empty string should be null if (parameter.isEmpty()) { setter.addValue(null); } else { setter.addValue(instantiatePluginModule(parameter)); } return 1; } @Override public String getDefaultMetaVariable() { return "com.foo.bar.BazModule"; } } /** * Parses command line flags written with args4j. * * @param objWithFlags An instance of a class containing args4j flag definitions. * @param args The args string to parse. * @param usagePrefix The string to prepend to the usage message (when reporting an error). * @return The CmdLineParser that was created and used to parse the args (can be used to print * usage text for flags when reporting errors). */ static CmdLineParser parseFlags(Object objWithFlags, String[] args, String usagePrefix) { CmdLineParser.registerHandler(Module.class, ModuleOptionHandler.class); // overwrite the built in boolean handler CmdLineParser.registerHandler(Boolean.class, BooleanOptionHandler.class); CmdLineParser.registerHandler(boolean.class, BooleanOptionHandler.class); CmdLineParser cmdLineParser = new CmdLineParser(objWithFlags); cmdLineParser.setUsageWidth(100); try { cmdLineParser.parseArgument(args); } catch (CmdLineException cle) { exitWithError(cle.getMessage(), cmdLineParser, usagePrefix); } return cmdLineParser; } @VisibleForTesting static int runInternal(Main method) { try { method.main(); return 0; } catch (SoyCompilationException compilationException) { System.err.println(compilationException.getMessage()); return 1; } catch (Exception e) { System.err.println( "INTERNAL SOY ERROR.\n" + "Please open an issue at " + "https://github.com/google/closure-templates/issues" + " with this stack trace and repro steps" ); e.printStackTrace(System.err); return 1; } } /** * Prints an error message and the usage string, and then exits. * * @param errorMsg The error message to print. * @param cmdLineParser The CmdLineParser used to print usage text for flags. * @param usagePrefix The string to prepend to the usage message (when reporting an error). */ static RuntimeException exitWithError( String errorMsg, CmdLineParser cmdLineParser, String usagePrefix) { System.err.println("\nError: " + errorMsg + "\n\n"); System.err.println(usagePrefix); cmdLineParser.printUsage(System.err); System.exit(1); throw new AssertionError(); // dead code } /** Returns a Guice injector that includes the SoyModule, and the given modules. */ static Injector createInjector(List<Module> modules) { modules = new ArrayList<>(modules); // make a copy that we know is mutable modules.add(new SoyModule()); return Guice.createInjector(modules); // TODO(lukes): Stage.PRODUCTION? } /** * Private helper for createInjector(). * * @param moduleName The name of the plugin module to instantiate. * @return A new instance of the specified plugin module. */ private static Module instantiatePluginModule(String moduleName) { try { return (Module) Class.forName(moduleName).getConstructor().newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException("Cannot instantiate plugin module \"" + moduleName + "\".", e); } } /** * Helper to add srcs and deps Soy files to a SoyFileSet builder. Also does sanity checks. * * @param sfsBuilder The SoyFileSet builder to add to. * @param inputPrefix The input path prefix to prepend to all the file paths. * @param srcs The srcs from the --srcs flag. Exactly one of 'srcs' and 'args' must be nonempty. * @param args The old-style srcs from the command line (that's how they were specified before we * added the --srcs flag). Exactly one of 'srcs' and 'args' must be nonempty. * @param deps The deps from the --deps flag, or empty list if not applicable. * @param exitWithErrorFn A function that exits with an error message followed by a usage message. */ static void addSoyFilesToBuilder( Builder sfsBuilder, String inputPrefix, Collection<String> srcs, Collection<String> args, Collection<String> deps, Collection<String> indirectDeps, Function<String, Void> exitWithErrorFn) { if (srcs.isEmpty() && args.isEmpty()) { exitWithErrorFn.apply("Must provide list of source Soy files (--srcs)."); } if (!srcs.isEmpty() && !args.isEmpty()) { exitWithErrorFn.apply( "Found source Soy files from --srcs and from args (please use --srcs only)."); } // Create Set versions of each of the arguments, and de-dupe. If something is included as // multiple file kinds, we'll keep the strongest one; a file in both srcs and deps will be a // src, and one in both deps and indirect_deps will be a dep. // TODO(gboyer): Maybe stop supporting old style (srcs from command line args) at some point. Set<String> srcsSet = ImmutableSet.<String>builder().addAll(srcs).addAll(args).build(); Set<String> depsSet = Sets.difference(ImmutableSet.copyOf(deps), srcsSet); Set<String> indirectDepsSet = Sets.difference(ImmutableSet.copyOf(indirectDeps), Sets.union(srcsSet, depsSet)); for (String src : srcsSet) { sfsBuilder.addWithKind(new File(inputPrefix + src), SoyFileKind.SRC); } for (String dep : depsSet) { sfsBuilder.addWithKind(new File(inputPrefix + dep), SoyFileKind.DEP); } for (String dep : indirectDepsSet) { sfsBuilder.addWithKind(new File(inputPrefix + dep), SoyFileKind.INDIRECT_DEP); } } }