PageRenderTime 27ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/core/src/main/java/com/atlassian/soy/impl/DefaultSoyManager.java

https://bitbucket.org/atlassian/atlassian-soy-templates
Java | 247 lines | 177 code | 35 blank | 35 comment | 16 complexity | 20835baf807dfa78cf665a8d48463fd5 MD5 | raw file
  1. package com.atlassian.soy.impl;
  2. import com.atlassian.annotations.tenancy.TenancyScope;
  3. import com.atlassian.annotations.tenancy.TenantAware;
  4. import com.atlassian.soy.impl.data.JavaBeanAccessorResolver;
  5. import com.atlassian.soy.renderer.SoyException;
  6. import com.atlassian.soy.spi.TemplateSetFactory;
  7. import com.atlassian.soy.spi.modules.GuiceModuleSupplier;
  8. import com.google.common.cache.CacheBuilder;
  9. import com.google.common.cache.CacheLoader;
  10. import com.google.common.cache.LoadingCache;
  11. import com.google.common.io.Closeables;
  12. import com.google.common.util.concurrent.UncheckedExecutionException;
  13. import com.google.inject.Injector;
  14. import com.google.template.soy.SoyFileSet;
  15. import com.google.template.soy.jssrc.SoyJsSrcOptions;
  16. import com.google.template.soy.shared.SoyAstCache;
  17. import com.google.template.soy.tofu.SoyTofu;
  18. import io.atlassian.util.concurrent.ResettableLazyReference;
  19. import org.slf4j.Logger;
  20. import org.slf4j.LoggerFactory;
  21. import javax.annotation.Nonnull;
  22. import java.io.IOException;
  23. import java.lang.reflect.InvocationHandler;
  24. import java.lang.reflect.Method;
  25. import java.lang.reflect.Proxy;
  26. import java.net.URL;
  27. import java.net.URLConnection;
  28. import java.util.List;
  29. import java.util.Map;
  30. import static com.atlassian.soy.impl.DevMode.isDevMode;
  31. public class DefaultSoyManager implements SoyManager {
  32. private static final SoyTofu DIDNOTCOMPILE = (SoyTofu) Proxy.newProxyInstance(
  33. DefaultSoyManager.class.getClassLoader(),
  34. new Class[]{SoyTofu.class},
  35. new NullTofuProxy()
  36. );
  37. private static final Logger log = LoggerFactory.getLogger(DefaultSoyManager.class);
  38. /**
  39. * Soy AST cache, to reduce recompilation of frequently-used templates and prevent caching multiple compiled
  40. * versions of the same source.
  41. * <p>
  42. * {@code SoyAstCache} has no mechanism for clearing cached values, so instead it is "cleared" by replacing
  43. * the entire instance. To reclaim memory, this needs entries need to be removed from {@link #soyTofuCache}
  44. * as well, since the compiled tofu may reference nodes from the AST cache.
  45. */
  46. @TenantAware(value = TenancyScope.TENANTLESS, comment = "Compiled SOY templates, same for all tenants.")
  47. private final ResettableLazyReference<SoyAstCache> soyAstCache;
  48. /**
  49. * Compiled Soy templates keyed on complete plugin-module key.
  50. */
  51. @TenantAware(value = TenancyScope.TENANTLESS, comment = "Compiled SOY templates, same for all tenants.")
  52. private final LoadingCache<String, SoyTofu> soyTofuCache;
  53. /**
  54. * Cache of the most recently modified time of the soy files in a plugin-module
  55. */
  56. @TenantAware(value = TenancyScope.TENANTLESS, comment = "Last modified time for templates, same for all tenants.")
  57. private final LoadingCache<String, Long> lastModifiedCache;
  58. private final JavaBeanAccessorResolver javaBeanAccessorResolver;
  59. private final SoyDependencyInjectorFactory soyDependencyInjectorFactory;
  60. private final TemplateSetFactory templateSetFactory;
  61. public DefaultSoyManager(GuiceModuleSupplier moduleSupplier,
  62. JavaBeanAccessorResolver javaBeanAccessorResolver,
  63. TemplateSetFactory templateSetFactory) {
  64. this.javaBeanAccessorResolver = javaBeanAccessorResolver;
  65. this.templateSetFactory = templateSetFactory;
  66. soyAstCache = new ResettableLazyReference<SoyAstCache>() {
  67. @Override
  68. protected SoyAstCache create() throws Exception {
  69. return new SoyAstCache();
  70. }
  71. };
  72. soyTofuCache = CacheBuilder.newBuilder()
  73. .build(new CacheLoader<String, SoyTofu>() {
  74. @Override
  75. public SoyTofu load(@Nonnull String key) throws SoyException {
  76. SoyTofu soyTofu = strainTofu(key);
  77. return soyTofu == null ? DIDNOTCOMPILE : soyTofu;
  78. }
  79. });
  80. lastModifiedCache = CacheBuilder.newBuilder()
  81. .build(new CacheLoader<String, Long>() {
  82. @Override
  83. public Long load(@Nonnull String key) {
  84. return getLastModifiedForModule(key);
  85. }
  86. });
  87. soyDependencyInjectorFactory = new SoyDependencyInjectorFactory(moduleSupplier);
  88. }
  89. @Override
  90. public String compile(final CharSequence content, final String filePath) {
  91. final SoyFileSet soyFiles = makeSoyFileSetBuilder()
  92. .setSupportContentSecurityPolicy(true)
  93. .add(content, filePath)
  94. .build();
  95. final SoyJsSrcOptions options = newOptions();
  96. List<String> output = soyFiles.compileToJsSrc(options, null);
  97. if (output.size() != 1) {
  98. throw new IllegalStateException("Did not manage to compile soy template at:" + filePath + ", size=" + output.size());
  99. }
  100. return output.get(0);
  101. }
  102. @Override
  103. public void render(Appendable appendable, String completeModuleKey, String templateName,
  104. Map<String, Object> data, Map<String, Object> injectedData) throws SoyException {
  105. if (isDevMode()) {
  106. log.debug("Clearing caches in dev mode");
  107. clearCaches(completeModuleKey);
  108. }
  109. try {
  110. SoyTofu tofu = soyTofuCache.getUnchecked(completeModuleKey);
  111. if (tofu == DIDNOTCOMPILE) {
  112. // Will only occur if there is a Soy exception compiling one of the templates for
  113. // this module.
  114. throw new SoyException("Unable to compile Soy template in plugin module: " + completeModuleKey);
  115. } else if (isDevMode()) {
  116. // SOY-27: if we successfully load a tofu, we need to record the last modified date of it
  117. // (ultimately soyTofuCache and lastModifiedCache should be kept in sync for a
  118. // particular completeModuleKey)
  119. lastModifiedCache.getUnchecked(completeModuleKey);
  120. }
  121. tofu.newRenderer(templateName)
  122. .setData(data)
  123. .setIjData(injectedData)
  124. .render(appendable);
  125. } catch (UncheckedExecutionException e) {
  126. throw new SoyException("Unable to compile Soy templates at: " + completeModuleKey, e.getCause());
  127. }
  128. }
  129. /**
  130. * @param completeModuleKey if <code>null</code>, will clear entire soy tofu cache
  131. */
  132. @Override
  133. public void clearCaches(String completeModuleKey) {
  134. // To ensure proper handling for updated plugins, whether a module key was provided or not the
  135. // AST cache needs to be cleared. Since the AST cache has no association between plugin modules
  136. // and locations, which are used as its cache keys, the only approach is to clear the entire
  137. // cache, which is done by using a new instance.
  138. soyAstCache.reset();
  139. soyDependencyInjectorFactory.clear();
  140. templateSetFactory.clear();
  141. javaBeanAccessorResolver.clearCaches();
  142. if (completeModuleKey == null) {
  143. soyTofuCache.invalidateAll();
  144. } else if (isModified(completeModuleKey)) {
  145. soyTofuCache.invalidate(completeModuleKey);
  146. // This is the last time it has been compiled - we need to clear it
  147. lastModifiedCache.invalidate(completeModuleKey);
  148. }
  149. }
  150. private long getLastModifiedForModule(String completeModuleKey) {
  151. long lastModified = 0;
  152. for (URL url : templateSetFactory.get(completeModuleKey)) {
  153. lastModified = Math.max(lastModified, getLastModified(url));
  154. }
  155. return lastModified;
  156. }
  157. private boolean isModified(String completeModuleKey) {
  158. try {
  159. final Long previousModifiedDate = lastModifiedCache.getUnchecked(completeModuleKey);
  160. final long currentModifiedDate = getLastModifiedForModule(completeModuleKey);
  161. return previousModifiedDate < currentModifiedDate || currentModifiedDate == -1;
  162. } catch (UncheckedExecutionException e) {
  163. log.debug("Unable to check resolve the module key '{}'. Treating as modified", completeModuleKey, e);
  164. return true;
  165. }
  166. }
  167. private static long getLastModified(URL url) {
  168. try {
  169. URLConnection urlConnection = url.openConnection();
  170. try {
  171. return urlConnection.getLastModified();
  172. } finally {
  173. // Don't leak underlying file handles
  174. Closeables.closeQuietly(urlConnection.getInputStream());
  175. }
  176. } catch (IOException e) {
  177. throw new RuntimeException(e);
  178. }
  179. }
  180. /*
  181. * Rebuilds the tofu that represents all the current soy web resource files registered in the plugin system.
  182. */
  183. private SoyTofu strainTofu(String completeModuleKey) throws SoyException {
  184. SoyFileSet.Builder builder = makeSoyFileSetBuilder()
  185. .setSupportContentSecurityPolicy(true)
  186. .setSoyAstCache(soyAstCache.get());
  187. templateSetFactory.get(completeModuleKey).forEach(builder::add);
  188. return builder.build().compileToTofu();
  189. }
  190. private SoyFileSet.Builder makeSoyFileSetBuilder() {
  191. Injector injector = soyDependencyInjectorFactory.get();
  192. return injector.getInstance(SoyFileSet.Builder.class);
  193. }
  194. private static SoyJsSrcOptions newOptions() {
  195. SoyJsSrcOptions options = new SoyJsSrcOptions();
  196. options.setShouldGenerateJsdoc(false);
  197. return options;
  198. }
  199. /**
  200. * {@code InvocationHandler} for dummy {@code SoyTofu} implementation.
  201. */
  202. private static class NullTofuProxy implements InvocationHandler {
  203. @Override
  204. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  205. // Support hashCode and equals methods
  206. if (Object.class.equals(method.getDeclaringClass())) {
  207. return method.invoke(this, args);
  208. }
  209. throw new UnsupportedOperationException();
  210. }
  211. }
  212. }