/*
* Copyright 2015 Google Inc.
*
* 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 com.google.template.soy.jbcsrc;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteSink;
import com.google.common.io.CharStreams;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.base.internal.SoyFileSupplier;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.jbcsrc.shared.CompiledTemplates;
import com.google.template.soy.jbcsrc.shared.Names;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
/** The entry point to the {@code jbcsrc} compiler. */
public final class BytecodeCompiler {
private static final Logger logger = Logger.getLogger(BytecodeCompiler.class.getName());
/**
* Compiles all the templates in the given registry.
*
* @param registry All the templates to compile
* @param developmentMode Whether or not we are in development mode. In development mode we
* compile classes lazily
* @param reporter The error reporter
* @return CompiledTemplates or {@code absent()} if compilation fails, in which case errors will
* have been reported to the error reporter.
*/
public static Optional<CompiledTemplates> compile(
final TemplateRegistry registry, boolean developmentMode, ErrorReporter reporter) {
final Stopwatch stopwatch = Stopwatch.createStarted();
ErrorReporter.Checkpoint checkpoint = reporter.checkpoint();
checkForUnsupportedFeatures(registry, reporter);
if (reporter.errorsSince(checkpoint)) {
return Optional.absent();
}
CompiledTemplateRegistry compilerRegistry = new CompiledTemplateRegistry(registry);
if (developmentMode) {
CompiledTemplates templates =
new CompiledTemplates(
compilerRegistry.getDelegateTemplateNames(),
new CompilingClassLoader(compilerRegistry));
// TODO(lukes): consider spawning a thread to load all the generated classes in the background
return Optional.of(templates);
}
// TODO(lukes): once most internal users have moved to precompilation eliminate this and just
// use the 'developmentMode' path above. This hybrid only makes sense for production services
// that are doing runtime compilation. Hopefully, this will become an anomaly.
List<ClassData> classes =
compileTemplates(
compilerRegistry,
reporter,
new CompilerListener<List<ClassData>>() {
final List<ClassData> compiledClasses = new ArrayList<>();
int numBytes = 0;
int numFields = 0;
int numDetachStates = 0;
@Override
public void onCompile(ClassData clazz) {
numBytes += clazz.data().length;
numFields += clazz.numberOfFields();
numDetachStates += clazz.numberOfDetachStates();
compiledClasses.add(clazz);
}
@Override
public List<ClassData> getResult() {
logger.log(
Level.INFO,
"Compilation took {0}\n"
+ " templates: {1}\n"
+ " classes: {2}\n"
+ " bytes: {3}\n"
+ " fields: {4}\n"
+ " detachStates: {5}",
new Object[] {
stopwatch.toString(),
registry.getAllTemplates().size(),
compiledClasses.size(),
numBytes,
numFields,
numDetachStates
});
return compiledClasses;
}
});
if (reporter.errorsSince(checkpoint)) {
return Optional.absent();
}
CompiledTemplates templates =
new CompiledTemplates(
compilerRegistry.getDelegateTemplateNames(), new MemoryClassLoader(classes));
stopwatch.reset().start();
templates.loadAll(compilerRegistry.getTemplateNames());
logger.log(Level.INFO, "Loaded all classes in {0}", stopwatch);
return Optional.of(templates);
}
/**
* Compiles all the templates in the given registry to a jar file written to the given output
* stream.
*
* <p>If errors are encountered, the error reporter will be updated and we will return. The
* contents of any data written to the sink at that point are undefined.
*
* @param registry All the templates to compile
* @param reporter The error reporter
* @param sink The output sink to write the JAR to.
*/
public static void compileToJar(TemplateRegistry registry, ErrorReporter reporter, ByteSink sink)
throws IOException {
ErrorReporter.Checkpoint checkpoint = reporter.checkpoint();
checkForUnsupportedFeatures(registry, reporter);
if (reporter.errorsSince(checkpoint)) {
return;
}
CompiledTemplateRegistry compilerRegistry = new CompiledTemplateRegistry(registry);
if (reporter.errorsSince(checkpoint)) {
return;
}
try (OutputStream stream = sink.openStream();
JarOutputStream jarOutput = new DeterministicJarOutputStream(stream, getJarManifest())) {
compileTemplates(
compilerRegistry,
reporter,
new CompilerListener<Void>() {
@Override
void onCompile(ClassData clazz) throws IOException {
jarOutput.putNextEntry(new ZipEntry(clazz.type().internalName() + ".class"));
jarOutput.write(clazz.data());
jarOutput.closeEntry();
}
});
}
}
/**
* Writes the source files out to a {@code -src.jar}. This places the soy files at the same
* classpath relative location as their generated classes. Ultimately this can be used by
* debuggers for source level debugging.
*
* <p>It is a little weird that the relative locations of the generated classes are not identical
* to the input source files. This is due to the disconnect between java packages and soy
* namespaces. We should consider using the soy namespace directly as a java package in the
* future.
*
* @param registry All the templates in the current compilation unit
* @param files The source files by file path
* @param sink The source to write the jar file
*/
public static void writeSrcJar(
TemplateRegistry registry, ImmutableMap<String, SoyFileSupplier> files, ByteSink sink)
throws IOException {
Set<SoyFileNode> seenFiles = new HashSet<>();
try (OutputStream stream = sink.openStream();
JarOutputStream jarOutput = new DeterministicJarOutputStream(stream, getJarManifest())) {
for (TemplateNode template : registry.getAllTemplates()) {
SoyFileNode file = template.getParent();
if (file.getSoyFileKind() == SoyFileKind.SRC && seenFiles.add(file)) {
String namespace = file.getNamespace();
String fileName = file.getFileName();
jarOutput.putNextEntry(new ZipEntry(Names.javaFileName(namespace, fileName)));
copyFileToOutput(files.get(file.getFilePath()), jarOutput);
jarOutput.closeEntry();
}
}
}
}
private static final class DeterministicJarOutputStream extends JarOutputStream {
DeterministicJarOutputStream(OutputStream outputStream, Manifest manifest) throws IOException {
super(outputStream, manifest);
}
@Override
public void putNextEntry(ZipEntry ze) throws IOException {
ze.setTime(0); // set an explicit timestamp to zero so we generate deterministic outputs
super.putNextEntry(ze);
}
}
/** Copies the file to the output stream */
private static void copyFileToOutput(SoyFileSupplier from, OutputStream to) throws IOException {
// 'from' contains a Reader which allows streaming reads of characters and 'to' is an
// OutputStream which allows for streaming writes of bytes. This disconnect means we need to do
// some character encoding. The classic way to do this is to use OutputStreamWriter to wrap the
// outputStream and apply an encoder. This introduces some wierdness because OutputStreamWriter
// can hold on to a few bytes to deal with unmatched surrogate pairs. So we would need to
// close/flush it inorder to not corrupt the files. This is undesirable since the output is
// actually a JarOutputStream and we are writing multiple files (we would over flush). So
// instead we do the naive thing and read the whole file as a string, convert the whole string
// to a byte array and then write the whole byte array.
//
// The real fix is to avoid the Reader and add methods to SoyFileSupplier to give us a
// ByteSource then we can avoid the error prone decode/encode dance.
String file;
try (Reader contents = from.open()) {
file = CharStreams.toString(contents);
}
to.write(file.getBytes(StandardCharsets.UTF_8));
}
/** Returns a simple jar manifest. */
private static Manifest getJarManifest() {
Manifest mf = new Manifest();
mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
mf.getMainAttributes().put(new Attributes.Name("Created-By"), "soy");
return mf;
}
private static void checkForUnsupportedFeatures(
TemplateRegistry registry, ErrorReporter errorReporter) {
UnsupportedFeatureReporter reporter = new UnsupportedFeatureReporter(errorReporter);
for (TemplateNode node : registry.getAllTemplates()) {
reporter.check(node);
}
}
private abstract static class CompilerListener<T> {
abstract void onCompile(ClassData newClass) throws Exception;
T getResult() {
return null;
}
}
private static <T> T compileTemplates(
CompiledTemplateRegistry registry,
ErrorReporter errorReporter,
CompilerListener<T> listener) {
for (String name : registry.getTemplateNames()) {
CompiledTemplateMetadata classInfo = registry.getTemplateInfoByTemplateName(name);
if (classInfo.node().getParent().getSoyFileKind() != SoyFileKind.SRC) {
continue; // only generate classes for sources
}
try {
TemplateCompiler templateCompiler = new TemplateCompiler(registry, classInfo);
for (ClassData clazz : templateCompiler.compile()) {
if (Flags.DEBUG) {
clazz.checkClass();
}
listener.onCompile(clazz);
}
// Report unexpected errors and keep going to try to collect more.
} catch (UnexpectedCompilerFailureException e) {
errorReporter.report(
e.getOriginalLocation(),
SoyErrorKind.of(
"Unexpected error while compiling template: ''{0}''\nSoy Stack:\n{1}"
+ "\nCompiler Stack:{2}"),
name,
e.printSoyStack(),
Throwables.getStackTraceAsString(e));
} catch (Throwable t) {
errorReporter.report(
classInfo.node().getSourceLocation(),
SoyErrorKind.of("Unexpected error while compiling template: ''{0}''\n{1}"),
name,
Throwables.getStackTraceAsString(t));
}
}
return listener.getResult();
}
private BytecodeCompiler() {}
}