package pl.matisoft.soy.ajax.process.google;
import javax.annotation.concurrent.ThreadSafe;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.logging.Level;
import com.google.common.collect.Lists;
import com.google.javascript.jscomp.*;
import com.google.javascript.jscomp.Compiler;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import pl.matisoft.soy.ajax.process.OutputProcessor;
import pl.matisoft.soy.config.SoyViewConfigDefaults;
/**
* Created with IntelliJ IDEA.
* User: mati
* Date: 06/10/2013
* Time: 15:57
*/
@ThreadSafe
public class GoogleClosureOutputProcessor implements OutputProcessor {
private static final Logger logger = LoggerFactory.getLogger(GoogleClosureOutputProcessor.class);
private String encoding = SoyViewConfigDefaults.DEFAULT_ENCODING;
private CompilationLevel compilationLevel = CompilationLevel.SIMPLE_OPTIMIZATIONS;
private boolean logCompilerErrors = true;
private boolean logCompilerWarnings = false;
@Override
public void process(final Reader reader, final Writer writer) throws IOException {
final String originalJsSourceCode = IOUtils.toString(reader);
try {
Compiler.setLoggingLevel(Level.SEVERE);
final Compiler compiler = new Compiler();
final CompilerOptions compilerOptions = newCompilerOptions();
compilationLevel.setOptionsForCompilationLevel(compilerOptions);
//make it play nice with GAE
compiler.disableThreads();
compiler.initOptions(compilerOptions);
final SourceFile input = SourceFile.fromInputStream("dummy.js", new ByteArrayInputStream(originalJsSourceCode.getBytes(getEncoding())));
final Result result = compiler.compile(Lists.<SourceFile>newArrayList(), Lists.newArrayList(input), compilerOptions);
logWarningsAndErrors(result);
boolean origFallback = false;
if (result.success) {
final String minifiedJsSourceCode = compiler.toSource();
if (StringUtils.isEmpty(minifiedJsSourceCode)) {
origFallback = true;
} else {
writer.write(minifiedJsSourceCode);
}
} else {
origFallback = true;
}
if (origFallback) {
writer.write(originalJsSourceCode);
}
} finally {
reader.close();
writer.close();
}
}
private void logWarningsAndErrors(final Result result) {
if (logCompilerErrors && result.errors.length > 0) {
for (JSError jsError : result.errors)
logger.warn("js error:" + jsError.toString());
}
if (logCompilerWarnings && result.warnings.length > 0) {
for (JSError jsError : result.warnings)
logger.warn("js warn:" + jsError.toString());
}
}
/**
* @return default {@link com.google.javascript.jscomp.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());
options.setOutputCharset(getEncoding());
//set it to warning, otherwise compiler will fail
options.setWarningLevel(DiagnosticGroups.CHECK_VARIABLES, CheckLevel.WARNING);
return options;
}
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setCompilationLevel(String compilationLevel) {
this.compilationLevel = CompilationLevel.valueOf(compilationLevel);
}
public CompilationLevel getCompilationLevel() {
return compilationLevel;
}
public void setLogCompilerErrors(boolean logCompilerErrors) {
this.logCompilerErrors = logCompilerErrors;
}
public void setLogCompilerWarnings(boolean logCompilerWarnings) {
this.logCompilerWarnings = logCompilerWarnings;
}
}