| /* |
| * 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.jasper.compiler; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FilePermission; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.net.URLDecoder; |
| import java.security.CodeSource; |
| import java.security.PermissionCollection; |
| import java.security.Policy; |
| import java.security.cert.Certificate; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.servlet.ServletContext; |
| |
| import org.apache.jasper.Constants; |
| import org.apache.jasper.JspCompilationContext; |
| import org.apache.jasper.Options; |
| import org.apache.jasper.runtime.ExceptionUtils; |
| import org.apache.jasper.servlet.JspServletWrapper; |
| import org.apache.jasper.util.FastRemovalDequeue; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| |
| |
| /** |
| * Class for tracking JSP compile time file dependencies when the |
| * >%@include file="..."%< directive is used. |
| * |
| * A background thread periodically checks the files a JSP page |
| * is dependent upon. If a dependent file changes the JSP page |
| * which included it is recompiled. |
| * |
| * Only used if a web application context is a directory. |
| * |
| * @author Glenn L. Nielsen |
| */ |
| public final class JspRuntimeContext { |
| |
| // Logger |
| private final Log log = LogFactory.getLog(JspRuntimeContext.class); |
| |
| /* |
| * Counts how many times the webapp's JSPs have been reloaded. |
| */ |
| private final AtomicInteger jspReloadCount = new AtomicInteger(0); |
| |
| /* |
| * Counts how many times JSPs have been unloaded in this webapp. |
| */ |
| private final AtomicInteger jspUnloadCount = new AtomicInteger(0); |
| |
| // ----------------------------------------------------------- Constructors |
| |
| /** |
| * Create a JspRuntimeContext for a web application context. |
| * |
| * Loads in any previously generated dependencies from file. |
| * |
| * @param context ServletContext for web application |
| */ |
| public JspRuntimeContext(ServletContext context, Options options) { |
| |
| this.context = context; |
| this.options = options; |
| |
| // Get the parent class loader |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| if (loader == null) { |
| loader = this.getClass().getClassLoader(); |
| } |
| |
| if (log.isDebugEnabled()) { |
| if (loader != null) { |
| log.debug(Localizer.getMessage("jsp.message.parent_class_loader_is", |
| loader.toString())); |
| } else { |
| log.debug(Localizer.getMessage("jsp.message.parent_class_loader_is", |
| "<none>")); |
| } |
| } |
| |
| parentClassLoader = loader; |
| classpath = initClassPath(); |
| |
| if (context instanceof org.apache.jasper.servlet.JspCServletContext) { |
| codeSource = null; |
| permissionCollection = null; |
| return; |
| } |
| |
| if (Constants.IS_SECURITY_ENABLED) { |
| SecurityHolder holder = initSecurity(); |
| codeSource = holder.cs; |
| permissionCollection = holder.pc; |
| } else { |
| codeSource = null; |
| permissionCollection = null; |
| } |
| |
| // If this web application context is running from a |
| // directory, start the background compilation thread |
| String appBase = context.getRealPath("/"); |
| if (!options.getDevelopment() |
| && appBase != null |
| && options.getCheckInterval() > 0) { |
| lastCompileCheck = System.currentTimeMillis(); |
| } |
| |
| if (options.getMaxLoadedJsps() > 0) { |
| jspQueue = new FastRemovalDequeue<>(options.getMaxLoadedJsps()); |
| if (log.isDebugEnabled()) { |
| log.debug(Localizer.getMessage("jsp.message.jsp_queue_created", |
| "" + options.getMaxLoadedJsps(), context.getContextPath())); |
| } |
| } |
| |
| /* Init parameter is in seconds, locally we use milliseconds */ |
| jspIdleTimeout = options.getJspIdleTimeout() * 1000; |
| } |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| /** |
| * This web applications ServletContext |
| */ |
| private final ServletContext context; |
| private final Options options; |
| private final ClassLoader parentClassLoader; |
| private final PermissionCollection permissionCollection; |
| private final CodeSource codeSource; |
| private final String classpath; |
| private volatile long lastCompileCheck = -1L; |
| private volatile long lastJspQueueUpdate = System.currentTimeMillis(); |
| /* JSP idle timeout in milliseconds */ |
| private long jspIdleTimeout; |
| |
| /** |
| * Maps JSP pages to their JspServletWrapper's |
| */ |
| private final Map<String, JspServletWrapper> jsps = new ConcurrentHashMap<>(); |
| |
| /** |
| * Keeps JSP pages ordered by last access. |
| */ |
| private FastRemovalDequeue<JspServletWrapper> jspQueue = null; |
| |
| // ------------------------------------------------------ Public Methods |
| |
| /** |
| * Add a new JspServletWrapper. |
| * |
| * @param jspUri JSP URI |
| * @param jsw Servlet wrapper for JSP |
| */ |
| public void addWrapper(String jspUri, JspServletWrapper jsw) { |
| jsps.put(jspUri, jsw); |
| } |
| |
| /** |
| * Get an already existing JspServletWrapper. |
| * |
| * @param jspUri JSP URI |
| * @return JspServletWrapper for JSP |
| */ |
| public JspServletWrapper getWrapper(String jspUri) { |
| return jsps.get(jspUri); |
| } |
| |
| /** |
| * Remove a JspServletWrapper. |
| * |
| * @param jspUri JSP URI of JspServletWrapper to remove |
| */ |
| public void removeWrapper(String jspUri) { |
| jsps.remove(jspUri); |
| } |
| |
| /** |
| * Push a newly compiled JspServletWrapper into the queue at first |
| * execution of jsp. Destroy any JSP that has been replaced in the queue. |
| * |
| * @param jsw Servlet wrapper for jsp. |
| * @return an unloadHandle that can be pushed to front of queue at later execution times. |
| * */ |
| public FastRemovalDequeue<JspServletWrapper>.Entry push(JspServletWrapper jsw) { |
| if (log.isTraceEnabled()) { |
| log.trace(Localizer.getMessage("jsp.message.jsp_added", |
| jsw.getJspUri(), context.getContextPath())); |
| } |
| FastRemovalDequeue<JspServletWrapper>.Entry entry = jspQueue.push(jsw); |
| JspServletWrapper replaced = entry.getReplaced(); |
| if (replaced != null) { |
| if (log.isDebugEnabled()) { |
| log.debug(Localizer.getMessage("jsp.message.jsp_removed_excess", |
| replaced.getJspUri(), context.getContextPath())); |
| } |
| unloadJspServletWrapper(replaced); |
| entry.clearReplaced(); |
| } |
| return entry; |
| } |
| |
| /** |
| * Push unloadHandle for JspServletWrapper to front of the queue. |
| * |
| * @param unloadHandle the unloadHandle for the jsp. |
| * */ |
| public void makeYoungest(FastRemovalDequeue<JspServletWrapper>.Entry unloadHandle) { |
| if (log.isTraceEnabled()) { |
| JspServletWrapper jsw = unloadHandle.getContent(); |
| log.trace(Localizer.getMessage("jsp.message.jsp_queue_update", |
| jsw.getJspUri(), context.getContextPath())); |
| } |
| jspQueue.moveFirst(unloadHandle); |
| } |
| |
| /** |
| * Returns the number of JSPs for which JspServletWrappers exist, i.e., |
| * the number of JSPs that have been loaded into the webapp. |
| * |
| * @return The number of JSPs that have been loaded into the webapp |
| */ |
| public int getJspCount() { |
| return jsps.size(); |
| } |
| |
| /** |
| * Get the SecurityManager Policy CodeSource for this web |
| * application context. |
| * |
| * @return CodeSource for JSP |
| */ |
| public CodeSource getCodeSource() { |
| return codeSource; |
| } |
| |
| /** |
| * Get the parent ClassLoader. |
| * |
| * @return ClassLoader parent |
| */ |
| public ClassLoader getParentClassLoader() { |
| return parentClassLoader; |
| } |
| |
| /** |
| * Get the SecurityManager PermissionCollection for this |
| * web application context. |
| * |
| * @return PermissionCollection permissions |
| */ |
| public PermissionCollection getPermissionCollection() { |
| return permissionCollection; |
| } |
| |
| /** |
| * Process a "destroy" event for this web application context. |
| */ |
| public void destroy() { |
| Iterator<JspServletWrapper> servlets = jsps.values().iterator(); |
| while (servlets.hasNext()) { |
| servlets.next().destroy(); |
| } |
| } |
| |
| /** |
| * Increments the JSP reload counter. |
| */ |
| public void incrementJspReloadCount() { |
| jspReloadCount.incrementAndGet(); |
| } |
| |
| /** |
| * Resets the JSP reload counter. |
| * |
| * @param count Value to which to reset the JSP reload counter |
| */ |
| public void setJspReloadCount(int count) { |
| jspReloadCount.set(count); |
| } |
| |
| /** |
| * Gets the current value of the JSP reload counter. |
| * |
| * @return The current value of the JSP reload counter |
| */ |
| public int getJspReloadCount() { |
| return jspReloadCount.intValue(); |
| } |
| |
| /** |
| * Gets the number of JSPs that are in the JSP limiter queue |
| * |
| * @return The number of JSPs (in the webapp with which this JspServlet is |
| * associated) that are in the JSP limiter queue |
| */ |
| public int getJspQueueLength() { |
| if (jspQueue != null) { |
| return jspQueue.getSize(); |
| } |
| return -1; |
| } |
| |
| /** |
| * Gets the number of JSPs that have been unloaded. |
| * |
| * @return The number of JSPs (in the webapp with which this JspServlet is |
| * associated) that have been unloaded |
| */ |
| public int getJspUnloadCount() { |
| return jspUnloadCount.intValue(); |
| } |
| |
| |
| /** |
| * Method used by background thread to check the JSP dependencies |
| * registered with this class for JSP's. |
| */ |
| public void checkCompile() { |
| |
| if (lastCompileCheck < 0) { |
| // Checking was disabled |
| return; |
| } |
| long now = System.currentTimeMillis(); |
| if (now > (lastCompileCheck + (options.getCheckInterval() * 1000L))) { |
| lastCompileCheck = now; |
| } else { |
| return; |
| } |
| |
| Object [] wrappers = jsps.values().toArray(); |
| for (int i = 0; i < wrappers.length; i++ ) { |
| JspServletWrapper jsw = (JspServletWrapper)wrappers[i]; |
| JspCompilationContext ctxt = jsw.getJspEngineContext(); |
| // JspServletWrapper also synchronizes on this when |
| // it detects it has to do a reload |
| synchronized(jsw) { |
| try { |
| ctxt.compile(); |
| } catch (FileNotFoundException ex) { |
| ctxt.incrementRemoved(); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| jsw.getServletContext().log("Background compile failed", |
| t); |
| } |
| } |
| } |
| |
| } |
| |
| /** |
| * The classpath that is passed off to the Java compiler. |
| */ |
| public String getClassPath() { |
| return classpath; |
| } |
| |
| /** |
| * Last time the update background task has run |
| */ |
| public long getLastJspQueueUpdate() { |
| return lastJspQueueUpdate; |
| } |
| |
| |
| // -------------------------------------------------------- Private Methods |
| |
| |
| /** |
| * Method used to initialize classpath for compiles. |
| */ |
| private String initClassPath() { |
| |
| StringBuilder cpath = new StringBuilder(); |
| |
| if (parentClassLoader instanceof URLClassLoader) { |
| URL [] urls = ((URLClassLoader)parentClassLoader).getURLs(); |
| |
| for (int i = 0; i < urls.length; i++) { |
| // Tomcat can use URLs other than file URLs. However, a protocol |
| // other than file: will generate a bad file system path, so |
| // only add file: protocol URLs to the classpath. |
| |
| if (urls[i].getProtocol().equals("file") ) { |
| try { |
| // Need to decode the URL, primarily to convert %20 |
| // sequences back to spaces |
| String decoded = URLDecoder.decode(urls[i].getPath(), "UTF-8"); |
| cpath.append(decoded + File.pathSeparator); |
| } catch (UnsupportedEncodingException e) { |
| // All JREs are required to support UTF-8 |
| } |
| } |
| } |
| } |
| |
| cpath.append(options.getScratchDir() + File.pathSeparator); |
| |
| String cp = (String) context.getAttribute(Constants.SERVLET_CLASSPATH); |
| if (cp == null || cp.equals("")) { |
| cp = options.getClassPath(); |
| } |
| |
| String path = cpath.toString() + cp; |
| |
| if(log.isDebugEnabled()) { |
| log.debug("Compilation classpath initialized: " + path); |
| } |
| return path; |
| } |
| |
| // Helper class to allow initSecurity() to return two items |
| private static class SecurityHolder{ |
| private final CodeSource cs; |
| private final PermissionCollection pc; |
| private SecurityHolder(CodeSource cs, PermissionCollection pc){ |
| this.cs = cs; |
| this.pc = pc; |
| } |
| } |
| /** |
| * Method used to initialize SecurityManager data. |
| */ |
| private SecurityHolder initSecurity() { |
| |
| // Setup the PermissionCollection for this web app context |
| // based on the permissions configured for the root of the |
| // web app context directory, then add a file read permission |
| // for that directory. |
| Policy policy = Policy.getPolicy(); |
| CodeSource source = null; |
| PermissionCollection permissions = null; |
| if( policy != null ) { |
| try { |
| // Get the permissions for the web app context |
| String docBase = context.getRealPath("/"); |
| if( docBase == null ) { |
| docBase = options.getScratchDir().toString(); |
| } |
| String codeBase = docBase; |
| if (!codeBase.endsWith(File.separator)){ |
| codeBase = codeBase + File.separator; |
| } |
| File contextDir = new File(codeBase); |
| URL url = contextDir.getCanonicalFile().toURI().toURL(); |
| source = new CodeSource(url,(Certificate[])null); |
| permissions = policy.getPermissions(source); |
| |
| // Create a file read permission for web app context directory |
| if (!docBase.endsWith(File.separator)){ |
| permissions.add |
| (new FilePermission(docBase,"read")); |
| docBase = docBase + File.separator; |
| } else { |
| permissions.add |
| (new FilePermission |
| (docBase.substring(0,docBase.length() - 1),"read")); |
| } |
| docBase = docBase + "-"; |
| permissions.add(new FilePermission(docBase,"read")); |
| |
| // Spec says apps should have read/write for their temp |
| // directory. This is fine, as no security sensitive files, at |
| // least any that the app doesn't have full control of anyway, |
| // will be written here. |
| String workDir = options.getScratchDir().toString(); |
| if (!workDir.endsWith(File.separator)){ |
| permissions.add |
| (new FilePermission(workDir,"read,write")); |
| workDir = workDir + File.separator; |
| } |
| workDir = workDir + "-"; |
| permissions.add(new FilePermission( |
| workDir,"read,write,delete")); |
| |
| // Allow the JSP to access org.apache.jasper.runtime.HttpJspBase |
| permissions.add( new RuntimePermission( |
| "accessClassInPackage.org.apache.jasper.runtime") ); |
| } catch(Exception e) { |
| context.log("Security Init for context failed",e); |
| } |
| } |
| return new SecurityHolder(source, permissions); |
| } |
| |
| private void unloadJspServletWrapper(JspServletWrapper jsw) { |
| removeWrapper(jsw.getJspUri()); |
| synchronized(jsw) { |
| jsw.destroy(); |
| } |
| jspUnloadCount.incrementAndGet(); |
| } |
| |
| |
| /** |
| * Method used by background thread to check if any JSP's should be unloaded. |
| */ |
| public void checkUnload() { |
| |
| if (log.isTraceEnabled()) { |
| int queueLength = -1; |
| if (jspQueue != null) { |
| queueLength = jspQueue.getSize(); |
| } |
| log.trace(Localizer.getMessage("jsp.message.jsp_unload_check", |
| context.getContextPath(), "" + jsps.size(), "" + queueLength)); |
| } |
| long now = System.currentTimeMillis(); |
| if (jspIdleTimeout > 0) { |
| long unloadBefore = now - jspIdleTimeout; |
| Object [] wrappers = jsps.values().toArray(); |
| for (int i = 0; i < wrappers.length; i++ ) { |
| JspServletWrapper jsw = (JspServletWrapper)wrappers[i]; |
| synchronized(jsw) { |
| if (jsw.getLastUsageTime() < unloadBefore) { |
| if (log.isDebugEnabled()) { |
| log.debug(Localizer.getMessage("jsp.message.jsp_removed_idle", |
| jsw.getJspUri(), context.getContextPath(), |
| "" + (now-jsw.getLastUsageTime()))); |
| } |
| if (jspQueue != null) { |
| jspQueue.remove(jsw.getUnloadHandle()); |
| } |
| unloadJspServletWrapper(jsw); |
| } |
| } |
| } |
| } |
| lastJspQueueUpdate = now; |
| } |
| } |