/*
* Copyright 2014 GoDataDriven B.V.
*
* 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 io.divolte.server.js;
import io.undertow.util.ETag;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import javax.annotation.ParametersAreNonnullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
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.ErrorManager;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.WarningLevel;
import static com.google.javascript.jscomp.CompilerOptions.LanguageMode.*;
@ParametersAreNonnullByDefault
public class JavaScriptResource {
private static final Logger logger = LoggerFactory.getLogger(JavaScriptResource.class);
private static final CompilationLevel COMPILATION_LEVEL = CompilationLevel.ADVANCED_OPTIMIZATIONS;
private final String resourceName;
private final ImmutableMap<String, Object> scriptConstants;
private final GzippableHttpBody entityBody;
public JavaScriptResource(final String resourceName,
final ImmutableMap<String, Object> scriptConstants,
final boolean debugMode) throws IOException {
this.resourceName = Objects.requireNonNull(resourceName);
this.scriptConstants = Objects.requireNonNull(scriptConstants);
logger.debug("Compiling JavaScript resource: {}", resourceName);
final Compiler compiler;
try (final InputStream is = getClass().getClassLoader().getResourceAsStream(resourceName)) {
compiler = compile(resourceName, is, scriptConstants, debugMode);
}
logger.info("Pre-compiled JavaScript source: {}", resourceName);
final Result result = compiler.getResult();
if (!result.success || 0 < result.warnings.length) {
throw new IllegalArgumentException("Javascript resource contains warnings and/or errors: " + resourceName);
}
final byte[] entityBytes = compiler.toSource().getBytes(StandardCharsets.UTF_8);
entityBody = new GzippableHttpBody(ByteBuffer.wrap(entityBytes), generateETag(entityBytes));
}
public String getResourceName() {
return resourceName;
}
protected ImmutableMap<String, Object> getScriptConstants() {
return scriptConstants;
}
public GzippableHttpBody getEntityBody() {
return entityBody;
}
private static Compiler compile(final String filename,
final InputStream javascript,
final ImmutableMap<String,Object> scriptConstants,
final boolean debugMode) throws IOException {
final CompilerOptions options = new CompilerOptions();
COMPILATION_LEVEL.setOptionsForCompilationLevel(options);
COMPILATION_LEVEL.setTypeBasedOptimizationOptions(options);
options.setEnvironment(CompilerOptions.Environment.BROWSER);
options.setLanguageIn(ECMASCRIPT5_STRICT);
options.setLanguageOut(ECMASCRIPT5_STRICT);
WarningLevel.VERBOSE.setOptionsForWarningLevel(options);
// Enable pseudo-compatible mode for debugging where the output is compiled but
// can be related more easily to the original JavaScript source.
if (debugMode) {
options.setPrettyPrint(true);
COMPILATION_LEVEL.setDebugOptionsForCompilationLevel(options);
}
options.setDefineReplacements(scriptConstants);
final SourceFile source = SourceFile.fromInputStream(filename, javascript, StandardCharsets.UTF_8);
final Compiler compiler = new Compiler();
final ErrorManager errorManager = new Slf4jErrorManager(compiler);
compiler.setErrorManager(errorManager);
// TODO: Use an explicit list of externs instead of the default browser set, to control compatibility.
final List<SourceFile> externs = CommandLineRunner.getBuiltinExterns(options.getEnvironment());
compiler.compile(externs, ImmutableList.of(source), options);
return compiler;
}
private static ETag generateETag(final byte[] entityBytes) {
final MessageDigest digester = createDigester();
final byte[] digest = digester.digest(entityBytes);
return new ETag(false, Base64.getEncoder().encodeToString(digest));
}
private static MessageDigest createDigester() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException("JRE missing mandatory digest: SHA-256", e);
}
}
}