/*
* 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.shared;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.jbcsrc.shared.TemplateMetadata.DelTemplateMetadata;
import com.google.template.soy.shared.internal.DelTemplateSelector;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
/** The result of template compilation. */
public final class CompiledTemplates {
private final ClassLoader loader;
private final ConcurrentHashMap<String, TemplateData> templateNameToFactory =
new ConcurrentHashMap<>();
private final DelTemplateSelector<TemplateData> selector;
public CompiledTemplates(ImmutableSet<String> delTemplateNames) {
this(delTemplateNames, CompiledTemplates.class.getClassLoader());
}
/**
* @param delTemplateNames The names of all the compiled deltemplates (the mangled names). This is
* needed to construct a valid deltemplate selector.
* @param loader The classloader that contains the classes
*/
public CompiledTemplates(ImmutableSet<String> delTemplateNames, ClassLoader loader) {
this.loader = checkNotNull(loader);
// We need to build the deltemplate selector eagerly.
DelTemplateSelector.Builder<TemplateData> builder = new DelTemplateSelector.Builder<>();
for (String delTemplateImplName : delTemplateNames) {
TemplateData data = getTemplateData(delTemplateImplName);
if (!data.delTemplateName.isPresent()) {
throw new IllegalArgumentException(
"Expected " + delTemplateImplName + " to be a deltemplate");
}
String delTemplateName = data.delTemplateName.get();
if (data.delPackage.isPresent()) {
String delpackage = data.delPackage.get();
TemplateData prev = builder.add(delTemplateName, delpackage, data.variant, data);
if (prev != null) {
throw new IllegalArgumentException(
String.format(
"Found multiple deltemplates with the same name (%s) and package (%s). %s and %s",
delTemplateName,
delpackage,
delTemplateImplName,
Names.soyTemplateNameFromJavaClassName(
prev.factory.getClass().getDeclaringClass().getName())));
}
} else {
TemplateData prev = builder.addDefault(delTemplateName, data.variant, data);
if (prev != null) {
throw new IllegalArgumentException(
String.format(
"Found multiple default deltemplates with the same name (%s). %s and %s",
delTemplateName,
delTemplateImplName,
Names.soyTemplateNameFromJavaClassName(
prev.factory.getClass().getDeclaringClass().getName())));
}
}
}
this.selector = builder.build();
}
/** Returns the strict content type of the template. */
public Optional<ContentKind> getTemplateContentKind(String name) {
return getTemplateData(name).kind;
}
/** Returns a factory for the given fully qualified template name. */
public CompiledTemplate.Factory getTemplateFactory(String name) {
return getTemplateData(name).factory;
}
/** Eagerly load all the given templates. */
public void loadAll(Iterable<String> templateNames) {
for (String templateName : templateNames) {
getTemplateData(templateName);
}
}
/**
* Returns the transitive closure of all the injected params that might be used by this template.
*/
public ImmutableSortedSet<String> getTransitiveIjParamsForTemplate(String templateName) {
TemplateData templateData = getTemplateData(templateName);
ImmutableSortedSet<String> transitiveIjParams = templateData.transitiveIjParams;
// racy-lazy init pattern. We may calculate this more than once, but that is fine because each
// time should calculate the same value.
if (transitiveIjParams != null) {
// early return, we already calculated this.
return transitiveIjParams;
}
Set<TemplateData> all = new HashSet<>();
collectTransitiveCallees(templateData, all);
ImmutableSortedSet.Builder<String> ijs = ImmutableSortedSet.naturalOrder();
for (TemplateData callee : all) {
ijs.addAll(callee.injectedParams);
}
transitiveIjParams = ijs.build();
// save the results
templateData.transitiveIjParams = transitiveIjParams;
return transitiveIjParams;
}
/** Returns an active delegate for the given name, variant and active package selector. */
@Nullable
CompiledTemplate.Factory selectDelTemplate(
String delTemplateName, String variant, Predicate<String> activeDelPackageSelector) {
TemplateData selectedTemplate =
selector.selectTemplate(delTemplateName, variant, activeDelPackageSelector);
return selectedTemplate == null ? null : selectedTemplate.factory;
}
private TemplateData getTemplateData(String name) {
checkNotNull(name);
TemplateData template = templateNameToFactory.get(name);
if (template == null) {
template = loadFactory(name, loader);
TemplateData old = templateNameToFactory.putIfAbsent(name, template);
if (old != null) {
return old;
}
}
return template;
}
private static TemplateData loadFactory(String name, ClassLoader loader) {
// We construct the factories via reflection to bridge the gap between generated and
// non-generated code. However, each factory only needs to be constructed once so the
// reflective cost isn't paid on a per render basis.
CompiledTemplate.Factory factory;
try {
String factoryName = Names.javaClassNameFromSoyTemplateName(name) + "$Factory";
Class<? extends CompiledTemplate.Factory> factoryClass =
Class.forName(factoryName, true /* run clinit */, loader)
.asSubclass(CompiledTemplate.Factory.class);
factory = factoryClass.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("No class was compiled for template: " + name, e);
} catch (ReflectiveOperationException e) {
// this should be impossible since our factories are public with a default constructor.
// TODO(lukes): failures of bytecode verification will propagate as Errors, we should
// consider catching them here to add information about our generated types. (e.g. add the
// class trace and a pointer on how to file a soy bug)
throw new AssertionError(e);
}
return new TemplateData(factory);
}
/** Adds all transitively called templates to {@code visited} */
private void collectTransitiveCallees(TemplateData templateData, Set<TemplateData> visited) {
if (!visited.add(templateData)) {
return; // avoids chasing recursive cycles
}
for (String callee : templateData.callees) {
collectTransitiveCallees(getTemplateData(callee), visited);
}
for (String delCallee : templateData.delCallees) {
// for {delcalls} we consider all possible targets
for (TemplateData potentialCallee : selector.delTemplateNameToValues().get(delCallee)) {
collectTransitiveCallees(potentialCallee, visited);
}
}
}
/** This is mostly a copy of the {@link TemplateMetadata} annotation. */
private static final class TemplateData {
final CompiledTemplate.Factory factory;
final Optional<ContentKind> kind;
final ImmutableSet<String> callees;
final ImmutableSet<String> delCallees;
final ImmutableSet<String> injectedParams;
// If this is a deltemplate then delTemplateName will be present
final Optional<String> delTemplateName;
final Optional<String> delPackage;
final String variant;
// Lazily initialized by getTransitiveIjParamsForTemplate. We initialize lazily because in
// general this is only needed for relatively few templates.
ImmutableSortedSet<String> transitiveIjParams;
TemplateData(CompiledTemplate.Factory factory) {
this.factory = factory;
// We pull the content kind off the templatemetadata eagerly since the parsing+reflection each
// time is expensive.
TemplateMetadata annotation =
factory.getClass().getDeclaringClass().getAnnotation(TemplateMetadata.class);
String contentKind = annotation.contentKind();
this.kind =
contentKind.isEmpty()
? Optional.<ContentKind>absent()
: Optional.of(ContentKind.valueOf(contentKind));
this.callees = ImmutableSet.copyOf(annotation.callees());
this.delCallees = ImmutableSet.copyOf(annotation.delCallees());
this.injectedParams = ImmutableSet.copyOf(annotation.injectedParams());
DelTemplateMetadata deltemplateMetadata = annotation.deltemplateMetadata();
variant = deltemplateMetadata.variant();
if (!deltemplateMetadata.name().isEmpty()) {
delTemplateName = Optional.of(deltemplateMetadata.name());
delPackage =
deltemplateMetadata.delPackage().isEmpty()
? Optional.<String>absent()
: Optional.of(deltemplateMetadata.delPackage());
} else {
this.delTemplateName = Optional.absent();
this.delPackage = Optional.absent();
}
}
}
}