be.crydust.jscompinplace.CompileInplaceTask.java Source code

Java tutorial

Introduction

Here is the source code for be.crydust.jscompinplace.CompileInplaceTask.java

Source

/*
 * 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 be.crydust.jscompinplace;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.Lists;
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.WarningLevel;
import com.google.javascript.jscomp.ant.AntErrorManager;
import com.google.javascript.jscomp.ant.Warning;

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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.Arrays;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;

/**
 * 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 CompileInplaceTask 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 generateExports;
    private boolean replaceProperties;
    private String replacePropertiesPrefix;
    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<FileSet> sourceFileSets;
    private final List<Warning> warnings;

    public CompileInplaceTask() {
        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.generateExports = false;
        this.replaceProperties = false;
        this.replacePropertiesPrefix = "closure.define.";
        this.defineParams = Lists.newLinkedList();
        this.entryPointParams = Lists.newLinkedList();
        this.externFileLists = Lists.newLinkedList();
        this.sourceFileLists = Lists.newLinkedList();
        this.sourcePaths = Lists.newLinkedList();
        this.sourceFileSets = Lists.newLinkedList();
        this.warnings = Lists.newLinkedList();
    }

    /**
     * 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 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;
    }

    /**
     * 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);
    }

    /**
     * Adds a <fileset/> entry.
     */
    public void addFileSet(FileSet fileSet) {
        this.sourceFileSets.add(fileSet);
    }

    @Override
    public void execute() {
        Compiler.setLoggingLevel(Level.OFF);

        CompilerOptions options = createCompilerOptions();

        List<SourceFile> externs = findExternFiles();
        List<SourceFile> sourceFiles = findSourceFiles();

        log("Compiling " + sourceFiles.size() + " file(s) with " + externs.size() + " extern(s)");

        for (SourceFile sourceFile : sourceFiles) {

            Compiler compiler = createCompiler(options);

            log("Compiling " + sourceFile.getOriginalPath());

            Result result = compiler.compile(externs, Arrays.asList(sourceFile), 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 + "'.");
                    }
                }

                writeResult(new File(sourceFile.getOriginalPath()), source.toString());
            } else {
                throw new BuildException("Compilation failed.");
            }
        }
    }

    private CompilerOptions createCompilerOptions() {
        CompilerOptions options = new CompilerOptions();

        this.compilationLevel.setOptionsForCompilationLevel(options);
        if (this.debugOptions) {
            this.compilationLevel.setDebugOptionsForCompilationLevel(options);
        }

        options.prettyPrint = this.prettyPrint;
        options.printInputDelimiter = this.printInputDelimiter;
        options.generateExports = this.generateExports;

        options.setLanguageIn(this.languageIn);
        options.setOutputCharset(this.outputEncoding);

        this.warningLevel.setOptionsForWarningLevel(options);
        options.setManageClosureDependencies(manageDependencies);
        convertEntryPointParameters(options);
        options.setTrustedStrings(true);

        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);
        }

        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 = Lists.newLinkedList();
        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.errorFormat.toFormatter(compiler, false);
        AntErrorManager errorManager = new AntErrorManager(formatter, this);
        compiler.setErrorManager(errorManager);
        return compiler;
    }

    private List<SourceFile> findExternFiles() {
        List<SourceFile> files = Lists.newLinkedList();
        if (!this.customExternsOnly) {
            files.addAll(getDefaultExterns());
        }

        for (FileList list : this.externFileLists) {
            files.addAll(findJavaScriptFiles(list));
        }

        return files;
    }

    private List<SourceFile> findSourceFiles() {
        List<SourceFile> files = Lists.newLinkedList();

        for (FileList list : this.sourceFileLists) {
            files.addAll(findJavaScriptFiles(list));
        }

        for (Path list : this.sourcePaths) {
            files.addAll(findJavaScriptFiles(list));
        }

        for (FileSet fileSet : this.sourceFileSets) {
            files.addAll(findJavaScriptFiles(fileSet));
        }

        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 = Lists.newLinkedList();
        Iterator<Resource> iter = rc.iterator();

        while (iter.hasNext()) {
            FileResource fr = iter.next().as(FileResource.class);
            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(File outputFile, String source) {
        if (outputFile.getParentFile().mkdirs()) {
            log("Created missing parent directory " + outputFile.getParentFile(), Project.MSG_DEBUG);
        }

        try {
            OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(outputFile), outputEncoding);
            out.append(source);
            out.flush();
            out.close();
        } catch (IOException e) {
            throw new BuildException(e);
        }

        log("Compiled JavaScript written to " + outputFile.getAbsolutePath(), Project.MSG_DEBUG);
    }

}