/* * Copyright 2014 the original author or 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 org.gradle.play.internal.javascript; import com.google.common.collect.Lists; import org.apache.commons.lang.StringUtils; import org.gradle.api.internal.file.RelativeFile; import org.gradle.api.internal.tasks.SimpleWorkResult; import org.gradle.api.tasks.WorkResult; import org.gradle.internal.Factory; import org.gradle.internal.UncheckedException; import org.gradle.internal.reflect.DirectInstantiator; import org.gradle.internal.reflect.JavaMethod; import org.gradle.internal.reflect.JavaReflectionUtil; import org.gradle.internal.reflect.PropertyAccessor; import org.gradle.language.base.internal.compile.Compiler; import org.gradle.plugins.javascript.base.SourceTransformationException; import org.gradle.util.GFileUtils; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.io.Serializable; import java.util.List; public class GoogleClosureCompiler implements Compiler<JavaScriptCompileSpec>, Serializable { private static final Iterable<String> SHARED_PACKAGES = Lists.newArrayList("com.google.javascript"); private static final String DEFAULT_GOOGLE_CLOSURE_VERSION = "v20141215"; private Class<?> sourceFileClass; private Class<?> compilerOptionsClass; private Class<Enum> compilationLevelClass; private Class<Object> compilerClass; public Iterable<String> getClassLoaderPackages() { return SHARED_PACKAGES; } public static Object getDependencyNotation() { return "com.google.javascript:closure-compiler:" + DEFAULT_GOOGLE_CLOSURE_VERSION; } @Override public WorkResult execute(JavaScriptCompileSpec spec) { JavaScriptCompileDestinationCalculator destinationCalculator = new JavaScriptCompileDestinationCalculator(spec.getDestinationDir()); List<String> allErrors = Lists.newArrayList(); for (RelativeFile sourceFile : spec.getSources()) { allErrors.addAll(compile(sourceFile, spec, destinationCalculator)); } if (allErrors.isEmpty()) { return new SimpleWorkResult(true); } else { throw new SourceTransformationException(String.format("Minification failed with the following errors:\n\t%s", StringUtils.join(allErrors, "\n\t")), null); } } List<String> compile(RelativeFile javascriptFile, JavaScriptCompileSpec spec, JavaScriptCompileDestinationCalculator destinationCalculator) { List<String> errors = Lists.newArrayList(); loadCompilerClasses(getClass().getClassLoader()); // Create a SourceFile object to represent an "empty" extern JavaMethod<?, Object> fromCodeJavaMethod = JavaReflectionUtil.staticMethod(sourceFileClass, Object.class, "fromCode", String.class, String.class); Object extern = fromCodeJavaMethod.invokeStatic("/dev/null", ""); // Create a SourceFile object to represent the javascript file to compile JavaMethod<?, Object> fromFileJavaMethod = JavaReflectionUtil.staticMethod(sourceFileClass, Object.class, "fromFile", File.class); Object sourceFile = fromFileJavaMethod.invokeStatic(javascriptFile.getFile()); // Construct a new CompilerOptions class Factory<?> compilerOptionsFactory = JavaReflectionUtil.factory(DirectInstantiator.INSTANCE, compilerOptionsClass); Object compilerOptions = compilerOptionsFactory.create(); // Get the CompilationLevel.SIMPLE_OPTIMIZATIONS class and set it on the CompilerOptions class @SuppressWarnings({ "rawtypes", "unchecked" }) Enum simpleLevel = Enum.valueOf(compilationLevelClass, "SIMPLE_OPTIMIZATIONS"); @SuppressWarnings("rawtypes") JavaMethod<Enum, Void> setOptionsForCompilationLevelMethod = JavaReflectionUtil.method(compilationLevelClass, Void.class, "setOptionsForCompilationLevel", compilerOptionsClass); setOptionsForCompilationLevelMethod.invoke(simpleLevel, compilerOptions); // Construct a new Compiler class Factory<?> compilerFactory = JavaReflectionUtil.factory(DirectInstantiator.INSTANCE, compilerClass, getDummyPrintStream()); Object compiler = compilerFactory.create(); // Compile the javascript file with the options we've created JavaMethod<Object, Object> compileMethod = JavaReflectionUtil.method(compilerClass, Object.class, "compile", sourceFileClass, sourceFileClass, compilerOptionsClass); Object result = compileMethod.invoke(compiler, extern, sourceFile, compilerOptions); // Get any errors from the compiler result PropertyAccessor<Object, Object[]> jsErrorsField = JavaReflectionUtil.readableField(result, Object[].class, "errors"); Object[] jsErrors = jsErrorsField.getValue(result); if (jsErrors.length == 0) { // If no errors, get the compiled source and write it to the destination file JavaMethod<Object, String> toSourceMethod = JavaReflectionUtil.method(compilerClass, String.class, "toSource"); String compiledSource = toSourceMethod.invoke(compiler); GFileUtils.writeFile(compiledSource, destinationCalculator.transform(javascriptFile)); } else { for (Object error : jsErrors) { errors.add(error.toString()); } } return errors; } private void loadCompilerClasses(ClassLoader cl) { try { if (sourceFileClass == null) { sourceFileClass = cl.loadClass("com.google.javascript.jscomp.SourceFile"); } if (compilerOptionsClass == null) { compilerOptionsClass = cl.loadClass("com.google.javascript.jscomp.CompilerOptions"); } if (compilationLevelClass == null) { @SuppressWarnings("unchecked") Class<Enum> clazz = (Class<Enum>) cl.loadClass("com.google.javascript.jscomp.CompilationLevel"); compilationLevelClass = clazz; } if (compilerClass == null) { @SuppressWarnings("unchecked") Class<Object> clazz = (Class<Object>) cl.loadClass("com.google.javascript.jscomp.Compiler"); compilerClass = clazz; } } catch (ClassNotFoundException e) { throw UncheckedException.throwAsUncheckedException(e); } } private PrintStream getDummyPrintStream() { OutputStream os = new OutputStream() { @Override public void write(int b) throws IOException { // do nothing } }; return new PrintStream(os); } }