/**
* Copyright@2010 Alex Objelean
*/
package ro.isdc.wro.extensions.processor.js;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Arrays;
import java.util.logging.Level;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.config.ReadOnlyContext;
import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.extensions.processor.support.ObjectPoolHelper;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.group.processor.Minimize;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.SupportedResourceType;
import ro.isdc.wro.model.resource.processor.Destroyable;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.util.ObjectFactory;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.ClosureCodingConvention;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.DiagnosticGroups;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceFile;
/**
* Uses Google closure compiler for js minimization.
* <p/>
* In order to make this class {@link Context} agnostic, set the encoding explicitly using
* {@link GoogleClosureCompressorProcessor#setEncoding(String)}.
*
* @see http://blog.bolinfest.com/2009/11/calling-closure-compiler-from-java.html
* @author Alex Objelean
*/
@Minimize
@SupportedResourceType(ResourceType.JS)
public class GoogleClosureCompressorProcessor
implements ResourcePostProcessor, ResourcePreProcessor, Destroyable {
public static final String ALIAS_SIMPLE = "googleClosureSimple";
public static final String ALIAS_ADVANCED = "googleClosureAdvanced";
public static final String ALIAS_WHITESPACE_ONLY = "googleClosureWhitespace";
/**
* {@link CompilationLevel} to use for compression.
*/
private CompilationLevel compilationLevel;
/**
* Reuse options(which are not thread safe).
*/
private ObjectPoolHelper<CompilerOptions> optionsPool;
@Inject
private ReadOnlyContext context;
private String encoding;
/**
* Uses google closure compiler with default compilation level: {@link CompilationLevel#SIMPLE_OPTIMIZATIONS}
*/
public GoogleClosureCompressorProcessor() {
this(CompilationLevel.SIMPLE_OPTIMIZATIONS);
}
/**
* Uses google closure compiler with specified compilation level.
*
* @param compilationLevel
* not null {@link CompilationLevel} enum.
*/
public GoogleClosureCompressorProcessor(final CompilationLevel compilationLevel) {
Validate.notNull(compilationLevel);
/**
* Using pool to fix the threadSafety issue. See <a
* href="http://code.google.com/p/closure-compiler/issues/detail?id=781">issue</a>.
*/
optionsPool = new ObjectPoolHelper<CompilerOptions>(new ObjectFactory<CompilerOptions>() {
@Override
public CompilerOptions create() {
return newCompilerOptions();
}
});
this.compilationLevel = compilationLevel;
}
/**
* {@inheritDoc}
*/
@Override
public void process(final Resource resource, final Reader reader, final Writer writer)
throws IOException {
final String content = IOUtils.toString(reader);
final CompilerOptions compilerOptions = optionsPool.getObject();
final Compiler compiler = newCompiler(compilerOptions);
try {
final String fileName = resource == null ? "wro4j-processed-file.js" : resource.getUri();
final SourceFile[] input = new SourceFile[] {
SourceFile.fromInputStream(fileName, new ByteArrayInputStream(content.getBytes(getEncoding())))
};
SourceFile[] externs = getExterns(resource);
if (externs == null) {
// fallback to empty array when null is provided.
externs = new SourceFile[] {};
}
Result result = null;
result = compiler.compile(Arrays.asList(externs), Arrays.asList(input), compilerOptions);
if (result.success) {
writer.write(compiler.toSource());
} else {
throw new WroRuntimeException("Compilation has errors: " + Arrays.asList(result.errors));
}
} catch (final Exception e) {
onException(e);
} finally {
reader.close();
writer.close();
optionsPool.returnObject(compilerOptions);
}
}
/**
* Invoked when an exception occurs during processing. Default implementation wraps the exception into
* {@link WroRuntimeException} and throws it further.
*
* @param e
* {@link Exception} thrown during processing.
*/
protected void onException(final Exception e) {
throw WroRuntimeException.wrap(e);
}
private String getEncoding() {
if (encoding == null) {
// use config is available to get encoding
this.encoding = Context.isContextSet() ? context.getConfig().getEncoding() : WroConfiguration.DEFAULT_ENCODING;
}
return encoding;
}
private Compiler newCompiler(final CompilerOptions compilerOptions) {
Compiler.setLoggingLevel(Level.SEVERE);
final Compiler compiler = new Compiler();
compilationLevel.setOptionsForCompilationLevel(compilerOptions);
// make it play nice with GAE
compiler.disableThreads();
compiler.initOptions(compilerOptions);
return compiler;
}
/**
* @param encoding
* the encoding to set
*/
public GoogleClosureCompressorProcessor setEncoding(final String encoding) {
this.encoding = encoding;
return this;
}
/**
* @param resource
* Currently processed resource. The resource can be null, when the closure compiler is used as a post
* processor.
* @return An Array of externs files for the resource to process.
*/
protected SourceFile[] getExterns(final Resource resource) {
return new SourceFile[] {};
}
/**
* @param compilationLevel
* the compilationLevel to set
*/
public GoogleClosureCompressorProcessor setCompilationLevel(final CompilationLevel compilationLevel) {
this.compilationLevel = compilationLevel;
return this;
}
/**
* @return default {@link CompilerOptions} object to be used by compressor.
*/
protected CompilerOptions newCompilerOptions() {
final CompilerOptions options = new CompilerOptions();
/**
* According to John Lenz from the Closure Compiler project, if you are using the Compiler API directly, you should
* specify a CodingConvention. {@link http://code.google.com/p/wro4j/issues/detail?id=155}
*/
options.setCodingConvention(new ClosureCodingConvention());
// use the wro4j encoding by default
options.setOutputCharset(getEncoding());
// set it to warning, otherwise compiler will fail
options.setWarningLevel(DiagnosticGroups.CHECK_VARIABLES, CheckLevel.WARNING);
return options;
}
/**
* {@inheritDoc}
*/
@Override
public void process(final Reader reader, final Writer writer)
throws IOException {
process(null, reader, writer);
}
@Override
public void destroy()
throws Exception {
optionsPool.destroy();
}
}