/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.portal.template.freemarker.internal;
import com.liferay.portal.configuration.metatype.bnd.util.ConfigurableUtil;
import com.liferay.portal.kernel.cache.SingleVMPool;
import com.liferay.portal.kernel.concurrent.ConcurrentReferenceKeyHashMap;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.memory.FinalizeManager;
import com.liferay.portal.kernel.servlet.JSPSupportServlet;
import com.liferay.portal.kernel.template.Template;
import com.liferay.portal.kernel.template.TemplateConstants;
import com.liferay.portal.kernel.template.TemplateException;
import com.liferay.portal.kernel.template.TemplateManager;
import com.liferay.portal.kernel.template.TemplateResource;
import com.liferay.portal.kernel.template.TemplateResourceLoader;
import com.liferay.portal.kernel.util.PropertiesUtil;
import com.liferay.portal.kernel.util.ProxyUtil;
import com.liferay.portal.kernel.util.ReflectionUtil;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.template.BaseSingleTemplateManager;
import com.liferay.portal.template.RestrictedTemplate;
import com.liferay.portal.template.TemplateContextHelper;
import com.liferay.portal.template.freemarker.configuration.FreeMarkerEngineConfiguration;
import freemarker.cache.TemplateCache;
import freemarker.core.TemplateClassResolver;
import freemarker.debug.impl.DebuggerService;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.ext.servlet.ServletContextHashModel;
import freemarker.template.Configuration;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.BundleTrackerCustomizer;
/**
* @author Mika Koivisto
* @author Tina Tina
* @author Raymond Augé
*/
@Component(
configurationPid = "com.liferay.portal.template.freemarker.configuration.FreeMarkerEngineConfiguration",
configurationPolicy = ConfigurationPolicy.OPTIONAL, immediate = true,
property = {"language.type=" + TemplateConstants.LANG_TYPE_FTL},
service = TemplateManager.class
)
public class FreeMarkerManager extends BaseSingleTemplateManager {
public static BeansWrapper getBeansWrapper() {
Thread currentThread = Thread.currentThread();
ClassLoader classLoader = currentThread.getContextClassLoader();
BeansWrapper beansWrapper = _beansWrappers.get(classLoader);
if (beansWrapper == null) {
BeansWrapperBuilder beansWrapperBuilder = new BeansWrapperBuilder(
Configuration.getVersion());
beansWrapper = beansWrapperBuilder.build();
_beansWrappers.put(classLoader, beansWrapper);
}
return beansWrapper;
}
@Override
public void addStaticClassSupport(
Map<String, Object> contextObjects, String variableName,
Class<?> variableClass) {
try {
BeansWrapper beansWrapper = getBeansWrapper();
TemplateHashModel templateHashModel =
beansWrapper.getStaticModels();
TemplateModel templateModel = templateHashModel.get(
variableClass.getName());
contextObjects.put(variableName, templateModel);
}
catch (TemplateModelException tme) {
if (_log.isWarnEnabled()) {
_log.warn(
"Variable " + variableName + " registration fail", tme);
}
}
}
@Override
public void addTaglibApplication(
Map<String, Object> contextObjects, String applicationName,
ServletContext servletContext) {
contextObjects.put(
applicationName, getServletContextHashModel(servletContext));
}
@Override
public void addTaglibFactory(
Map<String, Object> contextObjects, String taglibFactoryName,
ServletContext servletContext) {
contextObjects.put(
taglibFactoryName, new TaglibFactoryWrapper(servletContext));
}
@Override
public void addTaglibRequest(
Map<String, Object> contextObjects, String applicationName,
HttpServletRequest request, HttpServletResponse response) {
contextObjects.put(
applicationName,
new HttpRequestHashModel(
request, response, _configuration.getObjectWrapper()));
}
@Override
public void addTaglibSupport(
Map<String, Object> contextObjects, HttpServletRequest request,
HttpServletResponse response) {
ServletContext servletContext = request.getServletContext();
addTaglibApplication(contextObjects, "Application", servletContext);
addTaglibRequest(contextObjects, "Request", request, response);
// Legacy
TaglibFactoryWrapper taglibFactoryWrapper = new TaglibFactoryWrapper(
servletContext);
contextObjects.put("PortalJspTagLibs", taglibFactoryWrapper);
contextObjects.put("PortletJspTagLibs", taglibFactoryWrapper);
contextObjects.put("taglibLiferayHash", taglibFactoryWrapper);
// Contributed
for (Map.Entry<String, String> entry : _taglibMappings.entrySet()) {
try {
contextObjects.put(
entry.getKey(), taglibFactoryWrapper.get(entry.getValue()));
}
catch (TemplateModelException tme) {
_log.error(
"Unable to add taglib " + entry.getKey() + " to context",
tme);
}
}
}
@Override
public void destroy() {
if (_configuration == null) {
return;
}
_configuration.clearEncodingMap();
_configuration.clearSharedVariables();
_configuration.clearTemplateCache();
_configuration = null;
templateContextHelper.removeAllHelperUtilities();
templateContextHelper = null;
_templateModels.clear();
if (isEnableDebuggerService()) {
//DebuggerService.shutdown();
}
}
@Override
public void destroy(ClassLoader classLoader) {
templateContextHelper.removeHelperUtilities(classLoader);
}
@Override
public String getName() {
return TemplateConstants.LANG_TYPE_FTL;
}
@Override
public String[] getRestrictedVariables() {
return _freemarkerEngineConfiguration.restrictedVariables();
}
@Override
public void init() throws TemplateException {
if (_configuration != null) {
return;
}
_configuration = new Configuration(Configuration.getVersion());
try {
Field field = ReflectionUtil.getDeclaredField(
Configuration.class, "cache");
TemplateCache templateCache = new LiferayTemplateCache(
_configuration, _freemarkerEngineConfiguration,
templateResourceLoader, _singleVMPool);
field.set(_configuration, templateCache);
}
catch (Exception e) {
throw new TemplateException(
"Unable to Initialize FreeMarker manager");
}
_configuration.setDefaultEncoding(StringPool.UTF8);
_configuration.setLocalizedLookup(
_freemarkerEngineConfiguration.localizedLookup());
_configuration.setNewBuiltinClassResolver(_templateClassResolver);
_configuration.setObjectWrapper(new LiferayObjectWrapper());
try {
_configuration.setSetting(
"auto_import",
StringUtil.merge(
_freemarkerEngineConfiguration.macroLibrary()));
_configuration.setSetting(
"template_exception_handler",
_freemarkerEngineConfiguration.templateExceptionHandler());
}
catch (Exception e) {
throw new TemplateException("Unable to init FreeMarker manager", e);
}
if (isEnableDebuggerService()) {
DebuggerService.getBreakpoints("*");
}
}
@Reference(unbind = "-")
public void setTemplateClassResolver(
TemplateClassResolver templateClassResolver) {
_templateClassResolver = templateClassResolver;
}
@Override
@Reference(service = FreeMarkerTemplateContextHelper.class, unbind = "-")
public void setTemplateContextHelper(
TemplateContextHelper templateContextHelper) {
super.setTemplateContextHelper(templateContextHelper);
}
@Override
@Reference(service = FreeMarkerTemplateResourceLoader.class, unbind = "-")
public void setTemplateResourceLoader(
TemplateResourceLoader templateResourceLoader) {
super.setTemplateResourceLoader(templateResourceLoader);
}
@Activate
@Modified
protected void activate(ComponentContext componentContext) {
_freemarkerEngineConfiguration = ConfigurableUtil.createConfigurable(
FreeMarkerEngineConfiguration.class,
componentContext.getProperties());
BundleContext bundleContext = componentContext.getBundleContext();
_bundle = bundleContext.getBundle();
_freeMarkerBundleClassloader = new FreeMarkerBundleClassloader(_bundle);
int stateMask = Bundle.ACTIVE | Bundle.RESOLVED;
_bundleTracker = new BundleTracker<>(
bundleContext, stateMask, new TaglibBundleTrackerCustomizer());
_bundleTracker.open();
}
@Deactivate
protected void deactivate() {
_bundleTracker.close();
}
@Override
protected Template doGetTemplate(
TemplateResource templateResource,
TemplateResource errorTemplateResource, boolean restricted,
Map<String, Object> helperUtilities, boolean privileged) {
Template template = new FreeMarkerTemplate(
templateResource, errorTemplateResource, helperUtilities,
_configuration, templateContextHelper, privileged,
_freemarkerEngineConfiguration.resourceModificationCheck());
if (restricted) {
template = new RestrictedTemplate(
template, templateContextHelper.getRestrictedVariables());
}
return template;
}
protected ServletContextHashModel getServletContextHashModel(
ServletContext servletContext) {
GenericServlet genericServlet = new JSPSupportServlet(servletContext);
return new ServletContextHashModel(
genericServlet, _configuration.getObjectWrapper());
}
protected ServletContext getServletContextWrapper(
ServletContext servletContext) {
return (ServletContext)ProxyUtil.newProxyInstance(
_freeMarkerBundleClassloader, _INTERFACES,
new ServletContextInvocationHandler(servletContext));
}
protected boolean isEnableDebuggerService() {
if ((System.getProperty("freemarker.debug.password") != null) &&
(System.getProperty("freemarker.debug.port") != null)) {
return true;
}
return false;
}
@Reference(unbind = "-")
protected void setSingleVMPool(SingleVMPool singleVMPool) {
_singleVMPool = singleVMPool;
}
private static final Class<?>[] _INTERFACES = {ServletContext.class};
private static final Log _log = LogFactoryUtil.getLog(
FreeMarkerManager.class);
private static final Map<ClassLoader, BeansWrapper> _beansWrappers =
new ConcurrentReferenceKeyHashMap<>(
FinalizeManager.WEAK_REFERENCE_FACTORY);
private Bundle _bundle;
private BundleTracker<Set<String>> _bundleTracker;
private Configuration _configuration;
private volatile FreeMarkerBundleClassloader _freeMarkerBundleClassloader;
private volatile FreeMarkerEngineConfiguration
_freemarkerEngineConfiguration;
private SingleVMPool _singleVMPool;
private final Map<String, String> _taglibMappings =
new ConcurrentHashMap<>();
private TemplateClassResolver _templateClassResolver;
private final Map<String, TemplateModel> _templateModels =
new ConcurrentHashMap<>();
private class ServletContextInvocationHandler implements InvocationHandler {
public ServletContextInvocationHandler(ServletContext servletContext) {
_servletContext = servletContext;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if (methodName.equals("getClassLoader")) {
return _freeMarkerBundleClassloader;
}
else if (methodName.equals("getResource")) {
return _getResource((String)args[0]);
}
else if (methodName.equals("getResourceAsStream")) {
return _getResourceAsStream((String)args[0]);
}
else if (methodName.equals("getResourcePaths")) {
return _getResourcePaths((String)args[0]);
}
return method.invoke(_servletContext, args);
}
private URL _getExtension(String path) {
Enumeration<URL> enumeration = _bundle.findEntries(
"META-INF/resources", path.substring(1), false);
if (enumeration == null) {
return null;
}
List<URL> urls = Collections.list(enumeration);
return urls.get(urls.size() - 1);
}
private URL _getResource(String path) {
if (path.charAt(0) != '/') {
path = '/' + path;
}
URL url = _getExtension(path);
if (url != null) {
return url;
}
url = _freeMarkerBundleClassloader.getResource(path);
if (url != null) {
return url;
}
if (path.startsWith("/WEB-INF/tld/")) {
String adaptedPath =
"/META-INF/" + path.substring("/WEB-INF/tld/".length());
url = _getExtension(adaptedPath);
if (url == null) {
url = _bundle.getResource(adaptedPath);
}
}
if (url != null) {
return url;
}
if (!path.startsWith("/META-INF/") &&
!path.startsWith("/WEB-INF/")) {
url = _bundle.getResource("/META-INF/resources" + path);
}
return url;
}
private InputStream _getResourceAsStream(String path) {
URL url = _getResource(path);
if (url == null) {
return null;
}
try {
return url.openStream();
}
catch (IOException ioe) {
return null;
}
}
private Set<String> _getResourcePaths(String path) {
Enumeration<URL> entries = _bundle.findEntries(path, null, true);
if (entries == null) {
return null;
}
Set<String> resourcePaths = new HashSet<>();
while (entries.hasMoreElements()) {
URL url = entries.nextElement();
resourcePaths.add(url.getPath());
}
return resourcePaths;
}
private final ServletContext _servletContext;
}
private class TaglibBundleTrackerCustomizer
implements BundleTrackerCustomizer<Set<String>> {
@Override
public Set<String> addingBundle(
Bundle bundle, BundleEvent bundleEvent) {
boolean track = false;
Set<String> trackedKeys = new HashSet<>();
Enumeration<URL> enumeration = bundle.findEntries(
"/META-INF", "taglib-mappings.properties", true);
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
URL url = enumeration.nextElement();
try (InputStream inputStream = url.openStream()) {
Properties properties = PropertiesUtil.load(
inputStream, StringPool.UTF8);
Map<String, String> map = PropertiesUtil.toMap(
properties);
_taglibMappings.putAll(map);
trackedKeys.addAll(map.keySet());
track = true;
}
catch (Exception e) {
_log.error(e, e);
}
}
}
BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
List<BundleCapability> bundleCapabilities =
bundleWiring.getCapabilities("osgi.extender");
for (BundleCapability bundleCapability : bundleCapabilities) {
Map<String, Object> attributes =
bundleCapability.getAttributes();
Object value = attributes.get("osgi.extender");
if (value.equals("jsp.taglib")) {
_freeMarkerBundleClassloader.addBundle(bundle);
track = true;
break;
}
}
if (track) {
return trackedKeys;
}
return null;
}
@Override
public void modifiedBundle(
Bundle bundle, BundleEvent bundleEvent, Set<String> trackedKeys) {
}
@Override
public void removedBundle(
Bundle bundle, BundleEvent bundleEvent, Set<String> trackedKeys) {
_freeMarkerBundleClassloader.removeBundle(bundle);
for (String key : trackedKeys) {
_taglibMappings.remove(key);
}
_templateModels.clear();
}
}
private class TaglibFactoryWrapper implements TemplateHashModel {
public TaglibFactoryWrapper(ServletContext servletContext) {
_taglibFactory = new TaglibFactory(
getServletContextWrapper(servletContext));
_taglibFactory.setObjectWrapper(getBeansWrapper());
}
@Override
public TemplateModel get(String uri) throws TemplateModelException {
TemplateModel templateModel = _templateModels.get(uri);
if (templateModel == null) {
Thread currentThread = Thread.currentThread();
ClassLoader contextClassLoader =
currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(
_freeMarkerBundleClassloader);
templateModel = _taglibFactory.get(uri);
}
finally {
currentThread.setContextClassLoader(contextClassLoader);
}
_templateModels.put(uri, templateModel);
}
return templateModel;
}
@Override
public boolean isEmpty() {
return false;
}
private final TaglibFactory _taglibFactory;
}
}