/core/src/main/java/com/atlassian/soy/impl/DefaultSoyManager.java
Java | 247 lines | 177 code | 35 blank | 35 comment | 16 complexity | 20835baf807dfa78cf665a8d48463fd5 MD5 | raw file
- package com.atlassian.soy.impl;
- import com.atlassian.annotations.tenancy.TenancyScope;
- import com.atlassian.annotations.tenancy.TenantAware;
- import com.atlassian.soy.impl.data.JavaBeanAccessorResolver;
- import com.atlassian.soy.renderer.SoyException;
- import com.atlassian.soy.spi.TemplateSetFactory;
- import com.atlassian.soy.spi.modules.GuiceModuleSupplier;
- import com.google.common.cache.CacheBuilder;
- import com.google.common.cache.CacheLoader;
- import com.google.common.cache.LoadingCache;
- import com.google.common.io.Closeables;
- import com.google.common.util.concurrent.UncheckedExecutionException;
- import com.google.inject.Injector;
- import com.google.template.soy.SoyFileSet;
- import com.google.template.soy.jssrc.SoyJsSrcOptions;
- import com.google.template.soy.shared.SoyAstCache;
- import com.google.template.soy.tofu.SoyTofu;
- import io.atlassian.util.concurrent.ResettableLazyReference;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import javax.annotation.Nonnull;
- import java.io.IOException;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.lang.reflect.Proxy;
- import java.net.URL;
- import java.net.URLConnection;
- import java.util.List;
- import java.util.Map;
- import static com.atlassian.soy.impl.DevMode.isDevMode;
- public class DefaultSoyManager implements SoyManager {
- private static final SoyTofu DIDNOTCOMPILE = (SoyTofu) Proxy.newProxyInstance(
- DefaultSoyManager.class.getClassLoader(),
- new Class[]{SoyTofu.class},
- new NullTofuProxy()
- );
- private static final Logger log = LoggerFactory.getLogger(DefaultSoyManager.class);
- /**
- * Soy AST cache, to reduce recompilation of frequently-used templates and prevent caching multiple compiled
- * versions of the same source.
- * <p>
- * {@code SoyAstCache} has no mechanism for clearing cached values, so instead it is "cleared" by replacing
- * the entire instance. To reclaim memory, this needs entries need to be removed from {@link #soyTofuCache}
- * as well, since the compiled tofu may reference nodes from the AST cache.
- */
- @TenantAware(value = TenancyScope.TENANTLESS, comment = "Compiled SOY templates, same for all tenants.")
- private final ResettableLazyReference<SoyAstCache> soyAstCache;
- /**
- * Compiled Soy templates keyed on complete plugin-module key.
- */
- @TenantAware(value = TenancyScope.TENANTLESS, comment = "Compiled SOY templates, same for all tenants.")
- private final LoadingCache<String, SoyTofu> soyTofuCache;
- /**
- * Cache of the most recently modified time of the soy files in a plugin-module
- */
- @TenantAware(value = TenancyScope.TENANTLESS, comment = "Last modified time for templates, same for all tenants.")
- private final LoadingCache<String, Long> lastModifiedCache;
- private final JavaBeanAccessorResolver javaBeanAccessorResolver;
- private final SoyDependencyInjectorFactory soyDependencyInjectorFactory;
- private final TemplateSetFactory templateSetFactory;
- public DefaultSoyManager(GuiceModuleSupplier moduleSupplier,
- JavaBeanAccessorResolver javaBeanAccessorResolver,
- TemplateSetFactory templateSetFactory) {
- this.javaBeanAccessorResolver = javaBeanAccessorResolver;
- this.templateSetFactory = templateSetFactory;
- soyAstCache = new ResettableLazyReference<SoyAstCache>() {
- @Override
- protected SoyAstCache create() throws Exception {
- return new SoyAstCache();
- }
- };
- soyTofuCache = CacheBuilder.newBuilder()
- .build(new CacheLoader<String, SoyTofu>() {
- @Override
- public SoyTofu load(@Nonnull String key) throws SoyException {
- SoyTofu soyTofu = strainTofu(key);
- return soyTofu == null ? DIDNOTCOMPILE : soyTofu;
- }
- });
- lastModifiedCache = CacheBuilder.newBuilder()
- .build(new CacheLoader<String, Long>() {
- @Override
- public Long load(@Nonnull String key) {
- return getLastModifiedForModule(key);
- }
- });
- soyDependencyInjectorFactory = new SoyDependencyInjectorFactory(moduleSupplier);
- }
- @Override
- public String compile(final CharSequence content, final String filePath) {
- final SoyFileSet soyFiles = makeSoyFileSetBuilder()
- .setSupportContentSecurityPolicy(true)
- .add(content, filePath)
- .build();
- final SoyJsSrcOptions options = newOptions();
- List<String> output = soyFiles.compileToJsSrc(options, null);
- if (output.size() != 1) {
- throw new IllegalStateException("Did not manage to compile soy template at:" + filePath + ", size=" + output.size());
- }
- return output.get(0);
- }
- @Override
- public void render(Appendable appendable, String completeModuleKey, String templateName,
- Map<String, Object> data, Map<String, Object> injectedData) throws SoyException {
- if (isDevMode()) {
- log.debug("Clearing caches in dev mode");
- clearCaches(completeModuleKey);
- }
- try {
- SoyTofu tofu = soyTofuCache.getUnchecked(completeModuleKey);
- if (tofu == DIDNOTCOMPILE) {
- // Will only occur if there is a Soy exception compiling one of the templates for
- // this module.
- throw new SoyException("Unable to compile Soy template in plugin module: " + completeModuleKey);
- } else if (isDevMode()) {
- // SOY-27: if we successfully load a tofu, we need to record the last modified date of it
- // (ultimately soyTofuCache and lastModifiedCache should be kept in sync for a
- // particular completeModuleKey)
- lastModifiedCache.getUnchecked(completeModuleKey);
- }
- tofu.newRenderer(templateName)
- .setData(data)
- .setIjData(injectedData)
- .render(appendable);
- } catch (UncheckedExecutionException e) {
- throw new SoyException("Unable to compile Soy templates at: " + completeModuleKey, e.getCause());
- }
- }
- /**
- * @param completeModuleKey if <code>null</code>, will clear entire soy tofu cache
- */
- @Override
- public void clearCaches(String completeModuleKey) {
- // To ensure proper handling for updated plugins, whether a module key was provided or not the
- // AST cache needs to be cleared. Since the AST cache has no association between plugin modules
- // and locations, which are used as its cache keys, the only approach is to clear the entire
- // cache, which is done by using a new instance.
- soyAstCache.reset();
- soyDependencyInjectorFactory.clear();
- templateSetFactory.clear();
- javaBeanAccessorResolver.clearCaches();
- if (completeModuleKey == null) {
- soyTofuCache.invalidateAll();
- } else if (isModified(completeModuleKey)) {
- soyTofuCache.invalidate(completeModuleKey);
- // This is the last time it has been compiled - we need to clear it
- lastModifiedCache.invalidate(completeModuleKey);
- }
- }
- private long getLastModifiedForModule(String completeModuleKey) {
- long lastModified = 0;
- for (URL url : templateSetFactory.get(completeModuleKey)) {
- lastModified = Math.max(lastModified, getLastModified(url));
- }
- return lastModified;
- }
- private boolean isModified(String completeModuleKey) {
- try {
- final Long previousModifiedDate = lastModifiedCache.getUnchecked(completeModuleKey);
- final long currentModifiedDate = getLastModifiedForModule(completeModuleKey);
- return previousModifiedDate < currentModifiedDate || currentModifiedDate == -1;
- } catch (UncheckedExecutionException e) {
- log.debug("Unable to check resolve the module key '{}'. Treating as modified", completeModuleKey, e);
- return true;
- }
- }
- private static long getLastModified(URL url) {
- try {
- URLConnection urlConnection = url.openConnection();
- try {
- return urlConnection.getLastModified();
- } finally {
- // Don't leak underlying file handles
- Closeables.closeQuietly(urlConnection.getInputStream());
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- /*
- * Rebuilds the tofu that represents all the current soy web resource files registered in the plugin system.
- */
- private SoyTofu strainTofu(String completeModuleKey) throws SoyException {
- SoyFileSet.Builder builder = makeSoyFileSetBuilder()
- .setSupportContentSecurityPolicy(true)
- .setSoyAstCache(soyAstCache.get());
- templateSetFactory.get(completeModuleKey).forEach(builder::add);
- return builder.build().compileToTofu();
- }
- private SoyFileSet.Builder makeSoyFileSetBuilder() {
- Injector injector = soyDependencyInjectorFactory.get();
- return injector.getInstance(SoyFileSet.Builder.class);
- }
- private static SoyJsSrcOptions newOptions() {
- SoyJsSrcOptions options = new SoyJsSrcOptions();
- options.setShouldGenerateJsdoc(false);
- return options;
- }
- /**
- * {@code InvocationHandler} for dummy {@code SoyTofu} implementation.
- */
- private static class NullTofuProxy implements InvocationHandler {
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- // Support hashCode and equals methods
- if (Object.class.equals(method.getDeclaringClass())) {
- return method.invoke(this, args);
- }
- throw new UnsupportedOperationException();
- }
- }
- }