/** * 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.osgi.web.servlet.jsp.compiler; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.PropsUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.osgi.web.servlet.jsp.compiler.internal.JspBundleClassloader; import com.liferay.portal.osgi.web.servlet.jsp.compiler.internal.JspServletContext; import com.liferay.portal.osgi.web.servlet.jsp.compiler.internal.JspTagHandlerPool; import com.liferay.portal.servlet.delegate.ServletContextDelegate; import com.liferay.portal.util.PropsValues; import com.liferay.taglib.servlet.JspFactorySwapper; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.RequestDispatcher; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletContextAttributeListener; import javax.servlet.ServletContextListener; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestAttributeListener; import javax.servlet.ServletRequestListener; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionListener; import javax.servlet.jsp.JspFactory; import org.apache.felix.utils.log.Logger; import org.apache.jasper.runtime.JspFactoryImpl; import org.apache.jasper.runtime.TagHandlerPool; import org.apache.jasper.xmlparser.ParserUtils; import org.apache.jasper.xmlparser.TreeNode; import org.osgi.framework.Bundle; import org.osgi.framework.BundleEvent; import org.osgi.framework.BundleReference; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceRegistration; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; import org.osgi.util.tracker.BundleTracker; import org.osgi.util.tracker.BundleTrackerCustomizer; /** * @author Raymond Augé */ public class JspServlet extends HttpServlet { public static final String JSP_FILE = org.apache.jasper.Constants.JSP_FILE; public static void scanTLDs( Bundle bundle, ServletContext servletContext, List<String> listenerClassNames) { Boolean analyzedTlds = (Boolean)servletContext.getAttribute( _ANALYZED_TLDS); if ((analyzedTlds != null) && analyzedTlds.booleanValue()) { return; } servletContext.setAttribute(_ANALYZED_TLDS, Boolean.TRUE); BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); Collection<String> resources = bundleWiring.listResources( "META-INF/", "*.tld", BundleWiring.LISTRESOURCES_RECURSE); if (resources == null) { return; } for (String resource : resources) { URL url = bundle.getResource(resource); if (url == null) { continue; } try (InputStream inputStream = url.openStream()) { ParserUtils parserUtils = new ParserUtils(true); TreeNode treeNode = parserUtils.parseXMLDocument( url.getPath(), inputStream, false); Iterator<TreeNode> iterator = treeNode.findChildren("listener"); while (iterator.hasNext()) { TreeNode listenerTreeNode = iterator.next(); TreeNode listenerClassTreeNode = listenerTreeNode.findChild( "listener-class"); if (listenerClassTreeNode == null) { continue; } String listenerClassName = listenerClassTreeNode.getBody(); if (listenerClassName == null) { continue; } listenerClassNames.add(listenerClassName); } } catch (Exception e) { servletContext.log(e.getMessage(), e); } } } @Override public void destroy() { _jspServlet.destroy(); for (ServiceRegistration<?> serviceRegistration : _serviceRegistrations) { serviceRegistration.unregister(); } _serviceRegistrations.clear(); _bundleTracker.close(); } @Override public boolean equals(Object obj) { return _jspServlet.equals(obj); } @Override public String getInitParameter(String name) { return _jspServlet.getInitParameter(name); } @Override public Enumeration<String> getInitParameterNames() { return _jspServlet.getInitParameterNames(); } @Override public ServletConfig getServletConfig() { return _jspServlet.getServletConfig(); } @Override public ServletContext getServletContext() { return _jspServlet.getServletContext(); } @Override public String getServletInfo() { return _jspServlet.getServletInfo(); } @Override public String getServletName() { return _jspServlet.getServletName(); } @Override public int hashCode() { return _jspServlet.hashCode(); } @Override public void init() { throw new UnsupportedOperationException(); } @Override public void init(final ServletConfig servletConfig) throws ServletException { final ServletContext servletContext = ServletContextDelegate.create( servletConfig.getServletContext()); ClassLoader classLoader = servletContext.getClassLoader(); if (!(classLoader instanceof BundleReference)) { throw new IllegalStateException(); } Thread currentThread = Thread.currentThread(); ClassLoader contextClassLoader = currentThread.getContextClassLoader(); try { currentThread.setContextClassLoader(classLoader); JspFactory.setDefaultFactory(new JspFactoryImpl()); JspFactorySwapper.swap(); } finally { currentThread.setContextClassLoader(contextClassLoader); } List<Bundle> bundles = new ArrayList<>(); BundleReference bundleReference = (BundleReference)classLoader; _bundle = bundleReference.getBundle(); bundles.add(_bundle); bundles.add(_jspBundle); _logger = new Logger(_bundle.getBundleContext()); collectTaglibProviderBundles(bundles); _allParticipatingBundles = bundles.toArray(new Bundle[bundles.size()]); _jspBundleClassloader = new JspBundleClassloader( _allParticipatingBundles); final Map<String, String> defaults = new HashMap<>(); defaults.put( "compilerClassName", "com.liferay.portal.osgi.web.servlet.jsp.compiler.internal." + "JspCompiler"); defaults.put("compilerSourceVM", "1.8"); defaults.put("compilerTargetVM", "1.8"); defaults.put("development", "false"); defaults.put("httpMethods", "GET,POST,HEAD"); defaults.put("keepgenerated", "false"); defaults.put("logVerbosityLevel", "NONE"); defaults.put("saveBytecode", "true"); StringBundler sb = new StringBundler(4); sb.append(_WORK_DIR); sb.append(_bundle.getSymbolicName()); sb.append(StringPool.DASH); sb.append(_bundle.getVersion()); defaults.put(_INIT_PARAMETER_NAME_SCRATCH_DIR, sb.toString()); defaults.put( TagHandlerPool.OPTION_TAGPOOL, JspTagHandlerPool.class.getName()); for (Entry<Object, Object> entry : _initParams.entrySet()) { defaults.put( String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); } Enumeration<String> names = servletConfig.getInitParameterNames(); Set<String> nameSet = new HashSet<>(Collections.list(names)); nameSet.addAll(defaults.keySet()); final Enumeration<String> initParameterNames = Collections.enumeration( nameSet); _jspServlet.init( new ServletConfig() { @Override public String getInitParameter(String name) { String value = servletConfig.getInitParameter(name); if (value == null) { value = defaults.get(name); } return value; } @Override public Enumeration<String> getInitParameterNames() { return initParameterNames; } @Override public ServletContext getServletContext() { return _jspServletContext; } @Override public String getServletName() { return servletConfig.getServletName(); } private final ServletContext _jspServletContext = (ServletContext)Proxy.newProxyInstance( _jspBundleClassloader, _INTERFACES, new JspServletContextInvocationHandler( servletContext, _bundle)); }); _logVerbosityLevelDebug = Objects.equals( _jspServlet.getInitParameter("logVerbosityLevel"), "DEBUG"); _bundleTracker = new BundleTracker<>( _bundle.getBundleContext(), Bundle.RESOLVED, new JspFragmentTrackerCustomizer()); _bundleTracker.open(); } @Override public void log(String msg) { _jspServlet.log(msg); } @Override public void log(String message, Throwable t) { _jspServlet.log(message, t); } @Override public void service( HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Thread currentThread = Thread.currentThread(); ClassLoader contextClassLoader = currentThread.getContextClassLoader(); try { currentThread.setContextClassLoader(_jspBundleClassloader); if (_logVerbosityLevelDebug) { String path = (String)request.getAttribute( RequestDispatcher.INCLUDE_SERVLET_PATH); if (path != null) { String pathInfo = (String)request.getAttribute( RequestDispatcher.INCLUDE_PATH_INFO); if (pathInfo != null) { path += pathInfo; } } else { path = request.getServletPath(); String pathInfo = request.getPathInfo(); if (pathInfo != null) { path += pathInfo; } } _jspServlet.log("[JSP DEBUG] " + _bundle + " invoking " + path); } _jspServlet.service(request, response); } finally { currentThread.setContextClassLoader(contextClassLoader); } } @Override public void service(ServletRequest request, ServletResponse response) throws IOException, ServletException { service((HttpServletRequest)request, (HttpServletResponse)response); } @Override public String toString() { return _jspServlet.toString(); } protected void collectTaglibProviderBundles(List<Bundle> bundles) { BundleWiring bundleWiring = _bundle.adapt(BundleWiring.class); for (BundleWire bundleWire : bundleWiring.getRequiredWires("osgi.extender")) { BundleCapability bundleCapability = bundleWire.getCapability(); Map<String, Object> attributes = bundleCapability.getAttributes(); Object value = attributes.get("osgi.extender"); if (value.equals("jsp.taglib")) { BundleRevision bundleRevision = bundleWire.getProvider(); Bundle bundle = bundleRevision.getBundle(); if (!bundles.contains(bundle)) { bundles.add(bundle); } } } } protected String[] getListenerClassNames(Class<?> clazz) { List<String> classNames = new ArrayList<>(); if (ServletContextListener.class.isAssignableFrom(clazz)) { classNames.add(ServletContextListener.class.getName()); } if (ServletContextAttributeListener.class.isAssignableFrom(clazz)) { classNames.add(ServletContextAttributeListener.class.getName()); } if (ServletRequestListener.class.isAssignableFrom(clazz)) { classNames.add(ServletRequestListener.class.getName()); } if (ServletRequestAttributeListener.class.isAssignableFrom(clazz)) { classNames.add(ServletRequestAttributeListener.class.getName()); } if (HttpSessionListener.class.isAssignableFrom(clazz)) { classNames.add(HttpSessionListener.class.getName()); } if (HttpSessionAttributeListener.class.isAssignableFrom(clazz)) { classNames.add(HttpSessionAttributeListener.class.getName()); } if (classNames.isEmpty()) { throw new IllegalArgumentException( clazz.getName() + " does not implement one of the supported " + "servlet listener interfaces"); } return classNames.toArray(new String[classNames.size()]); } private static Map<Method, Method> _createContextAdapterMethods() { Map<Method, Method> methods = new HashMap<>(); Method[] adapterMethods = JspServletContextInvocationHandler.class.getDeclaredMethods(); for (Method adapterMethod : adapterMethods) { String name = adapterMethod.getName(); Class<?>[] parameterTypes = adapterMethod.getParameterTypes(); try { Method method = ServletContext.class.getMethod( name, parameterTypes); methods.put(method, adapterMethod); } catch (NoSuchMethodException nsme) { } } try { Method equalsMethod = Object.class.getMethod( "equals", Object.class); Method equalsHandlerMethod = JspServletContextInvocationHandler.class.getMethod( "equals", Object.class); methods.put(equalsMethod, equalsHandlerMethod); Method hashCodeMethod = Object.class.getMethod( "hashCode", (Class<?>[])null); Method hashCodeHandlerMethod = JspServletContextInvocationHandler.class.getMethod( "hashCode", (Class<?>[])null); methods.put(hashCodeMethod, hashCodeHandlerMethod); } catch (NoSuchMethodException nsme) { } return methods; } private void _deleteOutdatedJspFiles(String dir, List<Path> paths) { FileSystem fileSystem = FileSystems.getDefault(); Path dirPath = fileSystem.getPath(dir); if (Files.exists(dirPath) && !paths.isEmpty()) { try { Files.walkFileTree(dirPath, new DeleteFileVisitor(paths)); } catch (IOException ioe) { _logger.log( Logger.LOG_WARNING, "Unable to delete outdated files: " + paths); } } } private static final String _ANALYZED_TLDS = JspServlet.class.getName().concat("#ANALYZED_TLDS"); private static final String _DIR_NAME_RESOURCES = "/META-INF/resources"; private static final String _INIT_PARAMETER_NAME_SCRATCH_DIR = "scratchdir"; private static final Class<?>[] _INTERFACES = { JspServletContext.class, ServletContext.class }; private static final String _WORK_DIR = PropsValues.LIFERAY_HOME + File.separator + "work" + File.separator; private static final Map<Method, Method> _contextAdapterMethods; private static final Properties _initParams = PropsUtil.getProperties( "jsp.servlet.init.param.", true); private static final Bundle _jspBundle = FrameworkUtil.getBundle( JspServlet.class); private static final Pattern _originalJspPattern = Pattern.compile( "^(?<file>.*)(\\.(portal|original))(?<extension>\\.(jsp|jspf))$"); static { _contextAdapterMethods = _createContextAdapterMethods(); } private Bundle[] _allParticipatingBundles; private Bundle _bundle; private BundleTracker<List<Path>> _bundleTracker; private JspBundleClassloader _jspBundleClassloader; private final HttpServlet _jspServlet = new org.apache.jasper.servlet.JspServlet(); private Logger _logger; private boolean _logVerbosityLevelDebug; private final List<ServiceRegistration<?>> _serviceRegistrations = new CopyOnWriteArrayList<>(); private class DeleteFileVisitor extends SimpleFileVisitor<Path> { public DeleteFileVisitor(List<Path> paths) { _paths = paths; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (_paths.contains(file.toAbsolutePath())) { Files.delete(file); } return FileVisitResult.CONTINUE; } private final List<Path> _paths; } private class JspFragmentTrackerCustomizer implements BundleTrackerCustomizer<List<Path>> { @Override public List<Path> addingBundle(Bundle bundle, BundleEvent bundleEvent) { List<Path> paths = new ArrayList<>(); Dictionary<String, String> headers = bundle.getHeaders(); String fragmentHost = headers.get("Fragment-Host"); if (fragmentHost == null) { return null; } String[] fragmentHostParts = StringUtil.split( fragmentHost, CharPool.SEMICOLON); fragmentHost = fragmentHostParts[0]; String symbolicName = _bundle.getSymbolicName(); if (!symbolicName.equals(fragmentHost)) { return null; } Enumeration<URL> enumeration = bundle.findEntries( _DIR_NAME_RESOURCES, "*.jsp", true); if (enumeration == null) { return paths; } String scratchDirName = _jspServlet.getInitParameter( _INIT_PARAMETER_NAME_SCRATCH_DIR); while (enumeration.hasMoreElements()) { URL url = enumeration.nextElement(); String pathString = url.getPath(); if (pathString.startsWith(_DIR_NAME_RESOURCES)) { pathString = pathString.substring( _DIR_NAME_RESOURCES.length() + 1, pathString.length() - 4); } else { pathString = pathString.substring( 1, pathString.length() - 4); } pathString = StringUtil.replace( pathString, CharPool.UNDERLINE, "_005f"); paths.add( Paths.get( scratchDirName, "/org/apache/jsp/" + pathString + "_jsp.class")); } _deleteOutdatedJspFiles(scratchDirName, paths); return paths; } @Override public void modifiedBundle( Bundle bundle, BundleEvent bundleEvent, List<Path> paths) { } @Override public void removedBundle( Bundle bundle, BundleEvent bundleEvent, final List<Path> paths) { String scratchDirName = _jspServlet.getInitParameter( _INIT_PARAMETER_NAME_SCRATCH_DIR); _deleteOutdatedJspFiles(scratchDirName, paths); } } private class JspServletContextInvocationHandler implements InvocationHandler, JspServletContext { public JspServletContextInvocationHandler( ServletContext servletContext, Bundle bundle) { _servletContext = servletContext; _bundle = bundle; } @Override public boolean equals(Object obj) { if (!(obj instanceof ServletContext)) { return false; } ServletContext servletContext = (ServletContext)obj; if (obj instanceof JspServletContext) { JspServletContext jspServletContext = (JspServletContext)obj; servletContext = jspServletContext.getWrappedServletContext(); } return servletContext.equals(_servletContext); } @Override public ServletContext getWrappedServletContext() { return _servletContext; } @Override public int hashCode() { return _servletContext.hashCode(); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("getClassLoader")) { return _jspBundleClassloader; } else if (method.getName().equals("getResource")) { return _getResource((String)args[0]); } else if (method.getName().equals("getResourceAsStream")) { return _getResourceAsStream((String)args[0]); } else if (method.getName().equals("getResourcePaths")) { return _getResourcePaths((String)args[0]); } Method adapterMethod = _contextAdapterMethods.get(method); if (adapterMethod != null) { return adapterMethod.invoke(this, args); } return method.invoke(_servletContext, args); } private URL _getExtension(String path) { Matcher matcher = _originalJspPattern.matcher(path); if (matcher.matches()) { path = matcher.group("file") + matcher.group("extension"); return _bundle.getEntry(_DIR_NAME_RESOURCES + path); } Enumeration<URL> enumeration = _bundle.findEntries( _DIR_NAME_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) { try { if ((path == null) || path.equals(StringPool.BLANK)) { return null; } if (path.charAt(0) != '/') { path = '/' + path; } URL url = _getExtension(path); if (url != null) { return url; } url = _servletContext.getResource(path); if (url != null) { return url; } url = _servletContext.getClassLoader().getResource(path); if (url != null) { return url; } if (!path.startsWith("/META-INF/")) { url = _servletContext.getResource( _DIR_NAME_RESOURCES.concat(path)); } if (url != null) { return url; } for (int i = 2; i < _allParticipatingBundles.length; i++) { url = _allParticipatingBundles[i].getEntry(path); if (url != null) { return url; } } return _jspBundle.getResource(path); } catch (MalformedURLException murle) { } return null; } 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) { Set<String> paths = _servletContext.getResourcePaths(path); Enumeration<URL> enumeration = _jspBundle.findEntries( path, null, false); if (enumeration != null) { if ((paths == null) && enumeration.hasMoreElements()) { paths = new HashSet<>(); } while (enumeration.hasMoreElements()) { URL url = enumeration.nextElement(); paths.add(url.getPath()); } } return paths; } private final Bundle _bundle; private final ServletContext _servletContext; } }