/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.sling.scripting.thymeleaf.internal; import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Properties; import java.util.Set; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.scripting.api.AbstractScriptEngineFactory; import org.apache.sling.scripting.api.resource.ScriptingResourceResolverProvider; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.thymeleaf.ITemplateEngine; import org.thymeleaf.TemplateEngine; import org.thymeleaf.cache.ICacheManager; import org.thymeleaf.context.IEngineContextFactory; import org.thymeleaf.dialect.IDialect; import org.thymeleaf.linkbuilder.ILinkBuilder; import org.thymeleaf.linkbuilder.StandardLinkBuilder; import org.thymeleaf.messageresolver.IMessageResolver; import org.thymeleaf.messageresolver.StandardMessageResolver; import org.thymeleaf.standard.StandardDialect; import org.thymeleaf.templateparser.markup.decoupled.IDecoupledTemplateLogicResolver; import org.thymeleaf.templateresolver.ITemplateResolver; @Component( service = ScriptEngineFactory.class, immediate = true, property = { Constants.SERVICE_DESCRIPTION + "=ScriptEngineFactory for Sling Scripting Thymeleaf", Constants.SERVICE_VENDOR + "=The Apache Software Foundation" } ) @Designate( ocd = ThymeleafScriptEngineFactoryConfiguration.class ) public final class ThymeleafScriptEngineFactory extends AbstractScriptEngineFactory { @Reference( cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC, bind = "addTemplateResolver", unbind = "removeTemplateResolver" ) private List<ITemplateResolver> templateResolvers; @Reference( cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC, bind = "addMessageResolver", unbind = "removeMessageResolver" ) private List<IMessageResolver> messageResolvers; @Reference( cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC, bind = "addDialect", unbind = "removeDialect" ) private List<IDialect> dialects; @Reference( cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, bind = "addLinkBuilder", unbind = "removeLinkBuilder" ) private List<ILinkBuilder> linkBuilders; @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, bind = "setDecoupledTemplateLogicResolver", unbind = "unsetDecoupledTemplateLogicResolver" ) private volatile IDecoupledTemplateLogicResolver decoupledTemplateLogicResolver; @Reference( cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, bind = "setCacheManager", unbind = "unsetCacheManager" ) private volatile ICacheManager cacheManager; @Reference( policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, bind = "setEngineContextFactory", unbind = "unsetEngineContextFactory" ) private volatile IEngineContextFactory engineContextFactory; @Reference( policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY ) private volatile ScriptingResourceResolverProvider scriptingResourceResolverProvider; private ThymeleafScriptEngineFactoryConfiguration configuration; private BundleContext bundleContext; private TemplateEngine templateEngine; private ServiceRegistration<ITemplateEngine> serviceRegistration; private final Object lock = new Object(); private final String THYMELEAF_PROPERTIES = "/org/thymeleaf/thymeleaf.properties"; private final Logger logger = LoggerFactory.getLogger(ThymeleafScriptEngineFactory.class); public ThymeleafScriptEngineFactory() { } protected void addTemplateResolver(final ITemplateResolver templateResolver) { synchronized (lock) { logger.debug("adding template resolver '{}'", templateResolver.getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.addTemplateResolver(templateResolver); } } } protected void removeTemplateResolver(final ITemplateResolver templateResolver) { synchronized (lock) { logger.debug("removing template resolver '{}'", templateResolver.getName()); serviceTemplateEngine(); } } protected void addMessageResolver(final IMessageResolver messageResolver) { synchronized (lock) { logger.debug("adding message resolver '{}'", messageResolver.getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.addMessageResolver(messageResolver); } } } protected void removeMessageResolver(final IMessageResolver messageResolver) { synchronized (lock) { logger.debug("removing message resolver '{}'", messageResolver.getName()); serviceTemplateEngine(); } } protected void addDialect(final IDialect dialect) { synchronized (lock) { logger.debug("adding dialect '{}'", dialect.getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.addDialect(dialect); } } } protected void removeDialect(final IDialect dialect) { synchronized (lock) { logger.debug("removing dialect '{}'", dialect.getName()); serviceTemplateEngine(); } } protected void addLinkBuilder(final ILinkBuilder linkBuilder) { synchronized (lock) { logger.debug("adding link builder '{}'", linkBuilder.getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.addLinkBuilder(linkBuilder); } } } protected void removeLinkBuilder(final ILinkBuilder linkBuilder) { synchronized (lock) { logger.debug("removing link builder '{}'", linkBuilder.getName()); serviceTemplateEngine(); } } protected void setDecoupledTemplateLogicResolver(final IDecoupledTemplateLogicResolver decoupledTemplateLogicResolver) { synchronized (lock) { logger.debug("setting decoupled template logic resolver '{}'", decoupledTemplateLogicResolver.getClass().getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.setDecoupledTemplateLogicResolver(decoupledTemplateLogicResolver); } } } protected void unsetDecoupledTemplateLogicResolver(final IDecoupledTemplateLogicResolver decoupledTemplateLogicResolver) { synchronized (lock) { logger.debug("unsetting decoupled template logic resolver '{}'", decoupledTemplateLogicResolver.getClass().getName()); serviceTemplateEngine(); } } protected void setCacheManager(final ICacheManager cacheManager) { synchronized (lock) { logger.debug("setting cache manager '{}'", cacheManager.getClass().getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.setCacheManager(cacheManager); } } } protected void unsetCacheManager(final ICacheManager cacheManager) { synchronized (lock) { logger.debug("unsetting cache manager '{}'", cacheManager.getClass().getName()); serviceTemplateEngine(); } } protected void setEngineContextFactory(final IEngineContextFactory engineContextFactory) { synchronized (lock) { logger.debug("setting engine context factory '{}'", engineContextFactory.getClass().getName()); if (templateEngine == null || templateEngine.isInitialized()) { serviceTemplateEngine(); } else { templateEngine.setEngineContextFactory(engineContextFactory); } } } protected void unsetEngineContextFactory(final IEngineContextFactory engineContextFactory) { synchronized (lock) { logger.debug("unsetting engine context factory '{}'", engineContextFactory.getClass().getName()); serviceTemplateEngine(); } } @Activate private void activate(final ThymeleafScriptEngineFactoryConfiguration configuration, final BundleContext bundleContext) { logger.debug("activating"); this.configuration = configuration; this.bundleContext = bundleContext; configure(configuration); setupTemplateEngine(); registerTemplateEngine(); } @Modified private void modified(final ThymeleafScriptEngineFactoryConfiguration configuration) { logger.debug("modifying"); this.configuration = configuration; configure(configuration); } @Deactivate private void deactivate() { logger.debug("deactivating"); unregisterTemplateEngine(); templateEngine = null; bundleContext = null; } private void configure(final ThymeleafScriptEngineFactoryConfiguration configuration) { setExtensions(configuration.extensions()); setMimeTypes(configuration.mimeTypes()); setNames(configuration.names()); } @Override public String getLanguageName() { return "Thymeleaf"; } @Override public String getLanguageVersion() { try { final Properties properties = new Properties(); properties.load(getClass().getResourceAsStream(THYMELEAF_PROPERTIES)); return properties.getProperty("version"); } catch (Exception e) { logger.error("error reading version from " + THYMELEAF_PROPERTIES, e); return ""; // null breaks output of web console } } @Override public ScriptEngine getScriptEngine() { logger.debug("getting script engine for Thymeleaf"); return new ThymeleafScriptEngine(this); } private void serviceTemplateEngine() { unregisterTemplateEngine(); setupTemplateEngine(); registerTemplateEngine(); } private void setupTemplateEngine() { logger.info("setting up new template engine"); templateEngine = null; if (configuration == null) { logger.info("configuration is null, not setting up new template engine"); return; } if (!configuration.useStandardEngineContextFactory() && engineContextFactory == null) { logger.info("no engine context factory available, not setting up new template engine"); return; } // setup template engine final TemplateEngine templateEngine = new TemplateEngine(); // Template Resolvers if (this.templateResolvers != null) { final Set<ITemplateResolver> templateResolvers = new HashSet<>(this.templateResolvers); templateEngine.setTemplateResolvers(templateResolvers); } // Message Resolvers if (this.messageResolvers != null) { final Set<IMessageResolver> messageResolvers = new HashSet<>(this.messageResolvers); templateEngine.setMessageResolvers(messageResolvers); } if (configuration.useStandardMessageResolver()) { final IMessageResolver standardMessageResolver = new StandardMessageResolver(); templateEngine.addMessageResolver(standardMessageResolver); } // Link Builders if (this.linkBuilders != null) { final Set<ILinkBuilder> linkBuilders = new HashSet<>(this.linkBuilders); templateEngine.setLinkBuilders(linkBuilders); } if (configuration.useStandardLinkBuilder()) { final ILinkBuilder standardLinkBuilder = new StandardLinkBuilder(); templateEngine.addLinkBuilder(standardLinkBuilder); } // Dialects if (this.dialects != null) { final Set<IDialect> dialects = new HashSet<>(this.dialects); templateEngine.setDialects(dialects); } if (configuration.useStandardDialect()) { final IDialect standardDialect = new StandardDialect(); templateEngine.addDialect(standardDialect); } // Decoupled Template Logic Resolver if (!configuration.useStandardDecoupledTemplateLogicResolver()) { templateEngine.setDecoupledTemplateLogicResolver(decoupledTemplateLogicResolver); } // Cache Manager if (!configuration.useStandardCacheManager()) { templateEngine.setCacheManager(cacheManager); } // Engine Context Factory if (!configuration.useStandardEngineContextFactory()) { templateEngine.setEngineContextFactory(engineContextFactory); } // this.templateEngine = templateEngine; } private void registerTemplateEngine() { if (templateEngine == null || templateEngine.getTemplateResolvers().size() == 0 || templateEngine.getMessageResolvers().size() == 0 || templateEngine.getDialects().size() == 0) { return; } final Dictionary<String, String> properties = new Hashtable<>(); properties.put(Constants.SERVICE_DESCRIPTION, "Thymeleaf TemplateEngine"); properties.put(Constants.SERVICE_VENDOR, "The Thymeleaf Team"); logger.info("registering {} as service {} with properties {}", templateEngine, ITemplateEngine.class.getName(), properties); serviceRegistration = bundleContext.registerService(ITemplateEngine.class, templateEngine, properties); } private void unregisterTemplateEngine() { if (serviceRegistration != null) { serviceRegistration.unregister(); serviceRegistration = null; } } TemplateEngine getTemplateEngine() { synchronized (lock) { if (templateEngine == null) { serviceTemplateEngine(); } return templateEngine; } } ResourceResolver getRequestScopedResourceResolver() { return scriptingResourceResolverProvider.getRequestScopedResourceResolver(); } }