Java tutorial
/* * Copyright 2010 The Closure Compiler Authors. * * 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.javascript.jscomp.ant; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Strings; import com.google.common.io.Files; import com.google.javascript.jscomp.CheckLevel; import com.google.javascript.jscomp.CommandLineRunner; import com.google.javascript.jscomp.CompilationLevel; import com.google.javascript.jscomp.Compiler; import com.google.javascript.jscomp.CompilerOptions; import com.google.javascript.jscomp.DiagnosticGroup; import com.google.javascript.jscomp.DiagnosticGroups; import com.google.javascript.jscomp.MessageFormatter; import com.google.javascript.jscomp.Result; import com.google.javascript.jscomp.SourceFile; import com.google.javascript.jscomp.SourceMap; import com.google.javascript.jscomp.SourceMap.Format; import com.google.javascript.jscomp.SourceMap.LocationMapping; import com.google.javascript.jscomp.WarningLevel; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.FileList; import org.apache.tools.ant.types.Parameter; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.resources.FileResource; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.nio.file.Paths; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; /** * This class implements a simple Ant task to do almost the same as * CommandLineRunner. * * Most of the public methods of this class are entry points for the * Ant code to hook into. * */ public final class CompileTask extends Task { private CompilerOptions.LanguageMode languageIn; private WarningLevel warningLevel; private boolean debugOptions; private String encoding = "UTF-8"; private String outputEncoding = "UTF-8"; private CompilationLevel compilationLevel; private boolean customExternsOnly; private boolean manageDependencies; private boolean prettyPrint; private boolean printInputDelimiter; private boolean preferSingleQuotes; private boolean generateExports; private boolean replaceProperties; private boolean forceRecompile; private boolean angularPass; private String replacePropertiesPrefix; private File outputFile; private String outputWrapper; private File outputWrapperFile; private final List<Parameter> defineParams; private final List<Parameter> entryPointParams; private final List<FileList> externFileLists; private final List<FileList> sourceFileLists; private final List<Path> sourcePaths; private final List<Warning> warnings; private String sourceMapFormat; private File sourceMapOutputFile; private String sourceMapLocationMapping; public CompileTask() { this.languageIn = CompilerOptions.LanguageMode.ECMASCRIPT3; this.warningLevel = WarningLevel.DEFAULT; this.debugOptions = false; this.compilationLevel = CompilationLevel.SIMPLE_OPTIMIZATIONS; this.customExternsOnly = false; this.manageDependencies = false; this.prettyPrint = false; this.printInputDelimiter = false; this.preferSingleQuotes = false; this.generateExports = false; this.replaceProperties = false; this.forceRecompile = false; this.angularPass = false; this.replacePropertiesPrefix = "closure.define."; this.defineParams = new LinkedList(); this.entryPointParams = new LinkedList(); this.externFileLists = new LinkedList(); this.sourceFileLists = new LinkedList(); this.sourcePaths = new LinkedList(); this.warnings = new LinkedList(); } /** * Set the language to which input sources conform. * @param value The name of the language. * (ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT). */ public void setLanguageIn(String value) { switch (value) { case "ECMASCRIPT6_STRICT": case "ES6_STRICT": this.languageIn = CompilerOptions.LanguageMode.ECMASCRIPT6_STRICT; break; case "ECMASCRIPT6": case "ES6": this.languageIn = CompilerOptions.LanguageMode.ECMASCRIPT6; break; case "ECMASCRIPT5_STRICT": case "ES5_STRICT": this.languageIn = CompilerOptions.LanguageMode.ECMASCRIPT5_STRICT; break; case "ECMASCRIPT5": case "ES5": this.languageIn = CompilerOptions.LanguageMode.ECMASCRIPT5; break; case "ECMASCRIPT3": case "ES3": this.languageIn = CompilerOptions.LanguageMode.ECMASCRIPT3; break; default: throw new BuildException("Unrecognized 'languageIn' option value (" + value + ")"); } } /** * Set the warning level. * @param value The warning level by string name. (default, quiet, verbose). */ public void setWarning(String value) { if ("default".equalsIgnoreCase(value)) { this.warningLevel = WarningLevel.DEFAULT; } else if ("quiet".equalsIgnoreCase(value)) { this.warningLevel = WarningLevel.QUIET; } else if ("verbose".equalsIgnoreCase(value)) { this.warningLevel = WarningLevel.VERBOSE; } else { throw new BuildException("Unrecognized 'warning' option value (" + value + ")"); } } /** * Enable debugging options. * @param value True if debug mode is enabled. */ public void setDebug(boolean value) { this.debugOptions = value; } /** * Set the compilation level. * @param value The optimization level by string name. * (whitespace, simple, advanced). */ public void setCompilationLevel(String value) { if ("simple".equalsIgnoreCase(value)) { this.compilationLevel = CompilationLevel.SIMPLE_OPTIMIZATIONS; } else if ("advanced".equalsIgnoreCase(value)) { this.compilationLevel = CompilationLevel.ADVANCED_OPTIMIZATIONS; } else if ("whitespace".equalsIgnoreCase(value)) { this.compilationLevel = CompilationLevel.WHITESPACE_ONLY; } else { throw new BuildException("Unrecognized 'compilation' option value (" + value + ")"); } } public void setManageDependencies(boolean value) { this.manageDependencies = value; } /** * Use only custom externs. */ public void setCustomExternsOnly(boolean value) { this.customExternsOnly = value; } /** * Set output file. */ public void setOutput(File value) { this.outputFile = value; } /** * Set output wrapper. */ public void setOutputWrapper(String value) { this.outputWrapper = value; } /** * Set output wrapper file. */ public void setOutputWrapperFile(File value) { this.outputWrapperFile = value; } /** * Set the replacement property prefix. */ public void setReplacePropertiesPrefix(String value) { this.replacePropertiesPrefix = value; } /** * Whether to replace {@code @define} lines with properties */ public void setReplaceProperties(boolean value) { this.replaceProperties = value; } /** * Set input file encoding */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * Set output file encoding */ public void setOutputEncoding(String outputEncoding) { this.outputEncoding = outputEncoding; } /** * Set pretty print formatting option */ public void setPrettyPrint(boolean pretty) { this.prettyPrint = pretty; } /** * Set print input delimiter formatting option */ public void setPrintInputDelimiter(boolean print) { this.printInputDelimiter = print; } /** * Normally, when there are an equal number of single and double quotes * in a string, the compiler will use double quotes. Set this to true * to prefer single quotes. */ public void setPreferSingleQuotes(boolean singlequotes) { this.preferSingleQuotes = singlequotes; } /** * Set force recompile option */ public void setForceRecompile(boolean forceRecompile) { this.forceRecompile = forceRecompile; } public void setAngularPass(boolean angularPass) { this.angularPass = angularPass; } /** * Set generateExports option */ public void setGenerateExports(boolean generateExports) { this.generateExports = generateExports; } /** * Sets the externs file. */ public void addExterns(FileList list) { this.externFileLists.add(list); } /** * Adds a <warning/> entry * * Each warning entry must have two attributes, group and level. Group must * contain one of the constants from DiagnosticGroups (e.g., * "ACCESS_CONTROLS"), while level must contain one of the CheckLevel * constants ("ERROR", "WARNING" or "OFF"). */ public void addWarning(Warning warning) { this.warnings.add(warning); } /** * Adds a <entrypoint/> entry * * Each entrypoint entry must have one attribute, name. */ public void addEntryPoint(Parameter entrypoint) { this.entryPointParams.add(entrypoint); } /** * Sets the source files. */ public void addSources(FileList list) { this.sourceFileLists.add(list); } /** * Adds a <path/> entry. */ public void addPath(Path list) { this.sourcePaths.add(list); } @Override public void execute() { if (this.outputFile == null) { throw new BuildException("outputFile attribute must be set"); } Compiler.setLoggingLevel(Level.OFF); CompilerOptions options = createCompilerOptions(); Compiler compiler = createCompiler(options); List<SourceFile> externs = findExternFiles(); List<SourceFile> sources = findSourceFiles(); if (isStale() || forceRecompile) { log("Compiling " + sources.size() + " file(s) with " + externs.size() + " extern(s)"); Result result = compiler.compile(externs, sources, options); if (result.success) { StringBuilder source = new StringBuilder(compiler.toSource()); if (this.outputWrapperFile != null) { try { this.outputWrapper = Files.toString(this.outputWrapperFile, UTF_8); } catch (Exception e) { throw new BuildException("Invalid output_wrapper_file specified."); } } if (this.outputWrapper != null) { int pos = -1; pos = this.outputWrapper.indexOf(CommandLineRunner.OUTPUT_MARKER); if (pos > -1) { String prefix = this.outputWrapper.substring(0, pos); source.insert(0, prefix); // end of outputWrapper int suffixStart = pos + CommandLineRunner.OUTPUT_MARKER.length(); String suffix = this.outputWrapper.substring(suffixStart); source.append(suffix); } else { throw new BuildException("Invalid output_wrapper specified. " + "Missing '" + CommandLineRunner.OUTPUT_MARKER + "'."); } } if (result.sourceMap != null) { flushSourceMap(result.sourceMap); source.append(System.getProperty("line.separator")); source.append("//# sourceMappingURL=" + sourceMapOutputFile.getName()); } writeResult(source.toString()); } else { throw new BuildException("Compilation failed."); } } else { log("None of the files changed. Compilation skipped."); } } private void flushSourceMap(SourceMap sourceMap) { try { FileWriter out = new FileWriter(sourceMapOutputFile); sourceMap.appendTo(out, sourceMapOutputFile.getName()); out.close(); } catch (IOException e) { throw new BuildException("Cannot write sourcemap to file.", e); } } private CompilerOptions createCompilerOptions() { CompilerOptions options = new CompilerOptions(); this.compilationLevel.setOptionsForCompilationLevel(options); if (this.debugOptions) { this.compilationLevel.setDebugOptionsForCompilationLevel(options); } options.setPrettyPrint(this.prettyPrint); options.setPrintInputDelimiter(this.printInputDelimiter); options.setPreferSingleQuotes(this.preferSingleQuotes); options.setGenerateExports(this.generateExports); options.setLanguageIn(this.languageIn); options.setOutputCharset(this.outputEncoding); this.warningLevel.setOptionsForWarningLevel(options); options.setManageClosureDependencies(manageDependencies); convertEntryPointParameters(options); options.setTrustedStrings(true); options.setAngularPass(angularPass); if (replaceProperties) { convertPropertiesMap(options); } convertDefineParameters(options); for (Warning warning : warnings) { CheckLevel level = warning.getLevel(); String groupName = warning.getGroup(); DiagnosticGroup group = new DiagnosticGroups().forName(groupName); if (group == null) { throw new BuildException("Unrecognized 'warning' option value (" + groupName + ")"); } options.setWarningLevel(group, level); } if (!Strings.isNullOrEmpty(sourceMapFormat)) { options.setSourceMapFormat(Format.valueOf(sourceMapFormat)); } if (!Strings.isNullOrEmpty(sourceMapLocationMapping)) { String tokens[] = sourceMapLocationMapping.split("\\|", -1); LocationMapping lm = new LocationMapping(tokens[0], tokens[1]); options.setSourceMapLocationMappings(Arrays.asList(lm)); } if (sourceMapOutputFile != null) { File parentFile = sourceMapOutputFile.getParentFile(); if (parentFile.mkdirs()) { log("Created missing parent directory " + parentFile, Project.MSG_DEBUG); } options.setSourceMapOutputPath(parentFile.getAbsolutePath()); } return options; } /** * Creates a new {@code <define/>} nested element. Supports name and value * attributes. */ public Parameter createDefine() { Parameter param = new Parameter(); defineParams.add(param); return param; } /** * Creates a new {@code <entrypoint/>} nested element. Supports name * attribute. */ public Parameter createEntryPoint() { Parameter param = new Parameter(); entryPointParams.add(param); return param; } /** * Converts {@code <define/>} nested elements into Compiler {@code @define} * replacements. Note: unlike project properties, {@code <define/>} elements * do not need to be named starting with the replacement prefix. */ private void convertDefineParameters(CompilerOptions options) { for (Parameter p : defineParams) { String key = p.getName(); Object value = p.getValue(); if (!setDefine(options, key, value)) { log("Unexpected @define value for name=" + key + "; value=" + value); } } } /** * Converts {@code <entrypoint/>} nested elements into Compiler entrypoint * replacements. */ private void convertEntryPointParameters(CompilerOptions options) { List<String> entryPoints = new LinkedList(); for (Parameter p : entryPointParams) { String key = p.getName(); entryPoints.add(key); } if (this.manageDependencies) { options.setManageClosureDependencies(entryPoints); } } /** * Converts project properties beginning with the replacement prefix * into Compiler {@code @define} replacements. * * @param options */ private void convertPropertiesMap(CompilerOptions options) { @SuppressWarnings("unchecked") Map<String, Object> props = getProject().getProperties(); for (Map.Entry<String, Object> entry : props.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (key.startsWith(replacePropertiesPrefix)) { key = key.substring(replacePropertiesPrefix.length()); if (!setDefine(options, key, value)) { log("Unexpected property value for key=" + key + "; value=" + value); } } } } /** * Maps Ant-style values (e.g., from Properties) into expected * Closure {@code @define} literals * * @return True if the {@code @define} replacement succeeded, false if * the variable's value could not be mapped properly. */ private boolean setDefine(CompilerOptions options, String key, Object value) { boolean success = false; if (value instanceof String) { final boolean isTrue = "true".equals(value); final boolean isFalse = "false".equals(value); if (isTrue || isFalse) { options.setDefineToBooleanLiteral(key, isTrue); } else { try { double dblTemp = Double.parseDouble((String) value); options.setDefineToDoubleLiteral(key, dblTemp); } catch (NumberFormatException nfe) { // Not a number, assume string options.setDefineToStringLiteral(key, (String) value); } } success = true; } else if (value instanceof Boolean) { options.setDefineToBooleanLiteral(key, (Boolean) value); success = true; } else if (value instanceof Integer) { options.setDefineToNumberLiteral(key, (Integer) value); success = true; } else if (value instanceof Double) { options.setDefineToDoubleLiteral(key, (Double) value); success = true; } return success; } private Compiler createCompiler(CompilerOptions options) { Compiler compiler = new Compiler(); MessageFormatter formatter = options.getErrorFormat().toFormatter(compiler, false); AntErrorManager errorManager = new AntErrorManager(formatter, this); compiler.setErrorManager(errorManager); return compiler; } private List<SourceFile> findExternFiles() { List<SourceFile> files = new LinkedList(); if (!this.customExternsOnly) { files.addAll(getDefaultExterns()); } for (FileList list : this.externFileLists) { files.addAll(findJavaScriptFiles(list)); } return files; } private List<SourceFile> findSourceFiles() { List<SourceFile> files = new LinkedList(); for (FileList list : this.sourceFileLists) { files.addAll(findJavaScriptFiles(list)); } for (Path list : this.sourcePaths) { files.addAll(findJavaScriptFiles(list)); } return files; } /** * Translates an Ant resource collection into the file list format that * the compiler expects. */ private List<SourceFile> findJavaScriptFiles(ResourceCollection rc) { List<SourceFile> files = new LinkedList(); Iterator<Resource> iter = rc.iterator(); while (iter.hasNext()) { FileResource fr = (FileResource) iter.next(); // Construct path to file, relative to current working directory. File file = Paths.get("").toAbsolutePath().relativize(fr.getFile().toPath()).toFile(); files.add(SourceFile.fromFile(file, Charset.forName(encoding))); } return files; } /** * Gets the default externs set. * * Adapted from {@link CommandLineRunner}. */ private List<SourceFile> getDefaultExterns() { try { return CommandLineRunner.getDefaultExterns(); } catch (IOException e) { throw new BuildException(e); } } private void writeResult(String source) { if (this.outputFile.getParentFile().mkdirs()) { log("Created missing parent directory " + this.outputFile.getParentFile(), Project.MSG_DEBUG); } try { OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(this.outputFile), outputEncoding); out.append(source); out.flush(); out.close(); } catch (IOException e) { throw new BuildException(e); } log("Compiled JavaScript written to " + this.outputFile.getAbsolutePath(), Project.MSG_DEBUG); } /** * Determine if compilation must actually happen, i.e. if any input file * (extern or source) has changed after the outputFile was last modified. * * @return true if compilation should happen */ private boolean isStale() { long lastRun = outputFile.lastModified(); long sourcesLastModified = Math.max(getLastModifiedTime(this.sourceFileLists), getLastModifiedTime(this.sourcePaths)); long externsLastModified = getLastModifiedTime(this.externFileLists); return lastRun <= sourcesLastModified || lastRun <= externsLastModified; } /** * Returns the most recent modified timestamp of the file collection. * * Note: this must be combined into one method to account for both * Path and FileList erasure types. * * @param fileLists Collection of FileList or Path * @return Most recent modified timestamp */ private long getLastModifiedTime(List<?> fileLists) { long lastModified = 0; for (Object entry : fileLists) { if (entry instanceof FileList) { FileList list = (FileList) entry; for (String fileName : list.getFiles(this.getProject())) { File path = list.getDir(this.getProject()); File file = new File(path, fileName); lastModified = Math.max(getLastModifiedTime(file), lastModified); } } else if (entry instanceof Path) { Path path = (Path) entry; for (String src : path.list()) { File file = new File(src); lastModified = Math.max(getLastModifiedTime(file), lastModified); } } } return lastModified; } /** * Returns the last modified timestamp of the given File. */ private long getLastModifiedTime(File file) { long fileLastModified = file.lastModified(); // If the file is absent, we don't know if it changed (maybe was deleted), // so assume it has just changed. if (fileLastModified == 0) { fileLastModified = new Date().getTime(); } return fileLastModified; } public void setSourceMapFormat(String format) { this.sourceMapFormat = format; } public void setSourceMapOutputFile(File sourceMapOutputFile) { this.sourceMapOutputFile = sourceMapOutputFile; } public void setSourceMapLocationMapping(String mapping) { this.sourceMapLocationMapping = mapping; } }