/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.template.thymeleaf; import ognl.OgnlRuntime; import org.apache.felix.ipojo.annotations.*; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.thymeleaf.dialect.IDialect; import org.thymeleaf.messageresolver.IMessageResolver; import org.thymeleaf.templateresolver.TemplateResolver; import org.wisdom.api.asset.Assets; import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.router.Router; import org.wisdom.api.templates.Template; import org.wisdom.api.templates.TemplateEngine; import org.wisdom.template.thymeleaf.impl.ThymeLeafTemplateImplementation; import org.wisdom.template.thymeleaf.impl.WisdomTemplateEngine; import org.wisdom.template.thymeleaf.impl.WisdomURLResourceResolver; import java.io.File; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * The main component of the Thymeleaf template engine integration in Wisdom. */ @Component(immediate = true) @Provides(specifications = {ThymeleafTemplateCollector.class, TemplateEngine.class}) @Instantiate(name = "Thymeleaf template engine") public class ThymeleafTemplateCollector implements TemplateEngine { /** * The extension of the template supported by this engine. */ public static final String THYMELEAF_TEMPLATE_EXTENSION = "thl.html"; /** * The name of the template engine. */ public static final String THYMELEAF_ENGINE_NAME = "thymeleaf"; @Requires IMessageResolver messageResolver; private final BundleContext context; @Requires ApplicationConfiguration configuration; private static final Logger LOGGER = LoggerFactory.getLogger(ThymeleafTemplateCollector.class.getName()); private Map<ThymeLeafTemplateImplementation, ServiceRegistration<Template>> registrations = new ConcurrentHashMap<>(); /** * The internal engine. Accesses need to be synchronized as we change the engine instance when * dialects arrive and leave. */ WisdomTemplateEngine engine; @Requires private Router router; @Requires(optional = true) private Assets assets; Set<IDialect> dialects = new HashSet<>(); /** * Creates the collector. * * @param context the bundle context. This bundle context is used to registers the {@link org.wisdom.api * .templates.Template} services. */ public ThymeleafTemplateCollector(BundleContext context) { this.context = context; } /** * Stops the collector. This methods clear all registered {@link org.wisdom.api.templates.Template} services. */ @Invalidate public void stop() { for (ServiceRegistration<Template> reg : registrations.values()) { try { reg.unregister(); } catch (Exception e) { //NOSONAR // Ignore it. } } registrations.clear(); } /** * Updates the template object using the given file as backend. * * @param bundle the bundle containing the template, use system bundle for external templates. * @param templateFile the template file */ public void updatedTemplate(Bundle bundle, File templateFile) { ThymeLeafTemplateImplementation template = getTemplateByFile(templateFile); if (template != null) { LOGGER.debug("Thymeleaf template updated for {} ({})", templateFile.getAbsoluteFile(), template.fullName()); updatedTemplate(); } else { try { addTemplate(bundle, templateFile.toURI().toURL()); } catch (MalformedURLException e) { //NOSONAR // Ignored. } } } /** * Gets the template object using the given file as backend. * * @param templateFile the file * @return the template object, {@literal null} if not found */ private ThymeLeafTemplateImplementation getTemplateByFile(File templateFile) { try { return getTemplateByURL(templateFile.toURI().toURL()); } catch (MalformedURLException e) { //NOSONAR // Ignored. } return null; } /** * Gets the template object using the given url as backend. * * @param url the url * @return the template object, {@literal null} if not found */ private ThymeLeafTemplateImplementation getTemplateByURL(URL url) { Collection<ThymeLeafTemplateImplementation> list = registrations.keySet(); for (ThymeLeafTemplateImplementation template : list) { if (template.getURL().sameFile(url)) { return template; } } return null; } /** * Deletes the template using the given file as backend. * * @param templateFile the file */ public void deleteTemplate(File templateFile) { ThymeLeafTemplateImplementation template = getTemplateByFile(templateFile); if (template != null) { deleteTemplate(template); } } /** * Adds a template form the given url. * * @param bundle the bundle containing the template, use system bundle for external templates. * @param templateURL the url * @return the added template. IF the given url is already used by another template, return this other template. */ public ThymeLeafTemplateImplementation addTemplate(Bundle bundle, URL templateURL) { ThymeLeafTemplateImplementation template = getTemplateByURL(templateURL); if (template != null) { // Already existing. return template; } synchronized (this) { // need to be synchronized because of the access to engine. template = new ThymeLeafTemplateImplementation(engine, templateURL, router, assets, bundle); } ServiceRegistration<Template> reg = context.registerService(Template.class, template, template.getServiceProperties()); registrations.put(template, reg); LOGGER.debug("Thymeleaf template added for {}", templateURL.toExternalForm()); return template; } /** * Initializes the thymeleaf template engine. */ @Validate public synchronized void configure() { // Thymeleaf specifics String mode = configuration.getWithDefault("application.template.thymeleaf.mode", "HTML5"); int ttl = configuration.getIntegerWithDefault("application.template.thymeleaf.ttl", 60 * 1000); if (configuration.isDev()) { // In dev mode, reduce the ttl to the strict minimum so we are sure to have updated template rendering. ttl = 1; } LOGGER.debug("Thymeleaf configuration: mode={}, ttl={}", mode, ttl); // A TCCL switch is required here as the default Thymeleaf engine initialization triggers a class loading // from a class that may be present in the class path (org/apache/xerces/xni/parser/XMLParserConfiguration). // By setting the TCCL, it fails quietly, if not, it may find it but failed to instantiate it (version // mismatch or whatever). As this class is only used to support the HTML5LEGACY Templates (so not use here), // we don't really care. final ClassLoader orig = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); engine = new WisdomTemplateEngine(dialects); } finally { Thread.currentThread().setContextClassLoader(orig); } // Initiate the template resolver. TemplateResolver resolver = new TemplateResolver(); resolver.setResourceResolver(new WisdomURLResourceResolver(this)); resolver.setTemplateMode(mode); resolver.setCacheTTLMs((long) ttl); engine.setTemplateResolver(resolver); engine.setMessageResolver(messageResolver); engine.initialize(); } /** * A new dialect is now available. * @param dialect the dialect */ @Bind(optional = true, aggregate = true) public synchronized void bindDialect(IDialect dialect) { LOGGER.debug("Binding a new dialect using the prefix '{}' and containing {}", dialect.getPrefix(), dialect.getProcessors()); if (this.dialects.add(dialect)) { // We must reconfigure the engine configure(); // Update all templates. for (Template template : getTemplates()) { ((ThymeLeafTemplateImplementation) template).updateEngine(engine); } } } /** * A dialect has left. * @param dialect the dialect that has left */ @Unbind public synchronized void unbindDialect(IDialect dialect) { LOGGER.debug("Binding a new dialect {}, processors: {}", dialect.getPrefix(), dialect.getProcessors()); if (this.dialects.remove(dialect)) { configure(); for (Template template : getTemplates()) { ((ThymeLeafTemplateImplementation) template).updateEngine(engine); } } } /** * Gets the current list of templates. * * @return the current list of template */ @Override public Collection<Template> getTemplates() { return new ArrayList<>(registrations.keySet()); } /** * @return {@link #THYMELEAF_ENGINE_NAME}. */ @Override public String name() { return THYMELEAF_ENGINE_NAME; } /** * @return {@link #THYMELEAF_TEMPLATE_EXTENSION}. */ @Override public String extension() { return THYMELEAF_TEMPLATE_EXTENSION; } /** * Finds a template object from the given resource name. The first template matching the given name is returned. * * @param resourceName the name * @return the template object. */ public ThymeLeafTemplateImplementation getTemplateByResourceName(String resourceName) { Collection<ThymeLeafTemplateImplementation> list = registrations.keySet(); for (ThymeLeafTemplateImplementation template : list) { if (template.fullName().endsWith(resourceName) || template.fullName().endsWith(resourceName + "." + extension())) { return template; } if (template.name().equals(resourceName)) { return template; } } return null; } /** * Clears the cache when a template have been updated. */ public synchronized void updatedTemplate() { // Synchronized because of the access to engine. engine.getCacheManager().clearAllCaches(); } /** * Deletes the given template. The service is unregistered, and the cache is cleared. * * @param template the template */ public void deleteTemplate(ThymeLeafTemplateImplementation template) { // 1 - unregister the service try { ServiceRegistration reg = registrations.remove(template); if (reg != null) { reg.unregister(); } } catch (Exception e) { //NOSONAR // May already have been unregistered during the shutdown sequence. } // 2 - as templates can have dependencies, and expressions kept in memory, we clear all caches. // Despite this may really impact performance, it should not happen too often on real systems. synchronized (this) { engine.getCacheManager().clearAllCaches(); } OgnlRuntime.clearCache(); // Unfortunately, the previous method do not clear the get and set method cache // (ognl.OgnlRuntime.cacheGetMethod and ognl.OgnlRuntime.cacheSetMethod) clearMethodCaches(); } private void clearMethodCaches() { try { final Field cacheGetMethod = OgnlRuntime.class.getDeclaredField("cacheGetMethod"); final Field cacheSetMethod = OgnlRuntime.class.getDeclaredField("cacheSetMethod"); if (! cacheGetMethod.isAccessible()) { cacheGetMethod.setAccessible(true); } if (! cacheSetMethod.isAccessible()) { cacheSetMethod.setAccessible(true); } ((Map) cacheGetMethod.get(null)).clear(); ((Map) cacheSetMethod.get(null)).clear(); } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) { LOGGER.error("Cannot clean Thymeleaf cache, an exception has been thrown while clearing the Method " + "caches, this may introduce leaks", e); } } }