Index: test/org/apache/jasper/EmbeddedServletOptionsTest.java =================================================================== --- test/org/apache/jasper/EmbeddedServletOptionsTest.java (revision 0) +++ test/org/apache/jasper/EmbeddedServletOptionsTest.java (revision 0) @@ -0,0 +1,56 @@ +package org.apache.jasper; + +import javax.servlet.ServletContext; + +import org.apache.catalina.Container; +import org.apache.catalina.Engine; +import org.apache.catalina.core.ApplicationContext; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.core.StandardWrapper; + +import junit.framework.TestCase; + +/** + * Currently only tests whether maxLoadedJsps initializes correctly. + * */ +public class EmbeddedServletOptionsTest extends TestCase { + StandardWrapper config; + ServletContext context; + + protected void setUp() throws Exception { + super.setUp(); + config = new StandardWrapper(); + StandardContext std1 = new StandardContext(); + StandardContext std2 = new StandardContext(); + Engine std3 = new StandardEngine(); + std3.setService(new StandardService()); + std2.setParent(std3); + std1.setParent(std2); + context = new ApplicationContext(std1); + } + + public void testGetMaxLoadedJspsNegative() { + config.addInitParameter("maxLoadedJsps", "-1"); + EmbeddedServletOptions options = new EmbeddedServletOptions(config, context); + assertEquals(-1, options.getMaxLoadedJsps()); + } + + public void testGetMaxLoadedJspsDefault() { + EmbeddedServletOptions options = new EmbeddedServletOptions(config, context); + assertEquals(-1, options.getMaxLoadedJsps()); + } + + public void testGetMaxLoadedJspsPositive() { + config.addInitParameter("maxLoadedJsps", "2000"); + EmbeddedServletOptions options = new EmbeddedServletOptions(config, context); + assertEquals(2000, options.getMaxLoadedJsps()); + } + + public void testGetMaxLoadedJspsException() { + config.addInitParameter("maxLoadedJsps", "2000abc"); + EmbeddedServletOptions options = new EmbeddedServletOptions(config, context); + assertEquals(-1, options.getMaxLoadedJsps()); + } +} Index: test/org/apache/jasper/compiler/JspRuntimeContextTest.java =================================================================== --- test/org/apache/jasper/compiler/JspRuntimeContextTest.java (revision 0) +++ test/org/apache/jasper/compiler/JspRuntimeContextTest.java (revision 0) @@ -0,0 +1,86 @@ +package org.apache.jasper.compiler; + +import static org.easymock.EasyMock.*; + +import org.apache.catalina.Engine; +import org.apache.catalina.core.ApplicationContext; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.core.StandardWrapper; +import org.apache.jasper.EmbeddedServletOptions; +import org.apache.jasper.JasperException; +import org.apache.jasper.Options; +import org.apache.jasper.servlet.JspServletWrapper; + +import junit.framework.TestCase; + +public class JspRuntimeContextTest extends TestCase { + + JspRuntimeContext context; + ApplicationContext ctxt; + Options options; + + protected void setUp() throws Exception { + super.setUp(); + StandardWrapper config = new StandardWrapper(); + config.addInitParameter("maxLoadedJsps", "2"); + StandardContext std1 = new StandardContext(); + StandardContext std2 = new StandardContext(); + Engine std3 = new StandardEngine(); + std3.setService(new StandardService()); + std2.setParent(std3); + std1.setParent(std2); + ctxt = new ApplicationContext(std1); + options = new EmbeddedServletOptions(config, ctxt); + + context = new JspRuntimeContext(ctxt, options); + } + + public void testCheckUnloadDisabled() { + options = (Options) createMock(Options.class); + expect(options.getScratchDir()).andReturn(null); + expect(options.getClassPath()).andReturn(null); + expect(options.getDevelopment()).andReturn(false); + expect(options.getMaxLoadedJsps()).andReturn(-1); + replay(options); + context = new JspRuntimeContext(ctxt, options); + context.checkUnload(); + verify(options); + } + + public void testCheckUnloadEnabledSizeTooFewJsps() { + options = (Options) createMock(Options.class); + expect(options.getScratchDir()).andReturn(null); + expect(options.getClassPath()).andReturn(null); + expect(options.getDevelopment()).andReturn(false); + expect(options.getMaxLoadedJsps()).andReturn(1); + expect(options.getCheckInterval()).andReturn(0); + expect(options.getMaxLoadedJsps()).andReturn(1); + replay(options); + context = new JspRuntimeContext(ctxt, options); + context.checkUnload(); + verify(options); + } + + public void testCheckUnloadEnabledSizeEnoughJsps() throws JasperException { + options = (Options) createMock(Options.class); + expect(options.getScratchDir()).andReturn(null); + expect(options.getClassPath()).andReturn(null); + expect(options.getDevelopment()).andReturn(false); + expect(options.getMaxLoadedJsps()).andReturn(1); + expect(options.getCheckInterval()).andReturn(0); + expect(options.getMaxLoadedJsps()).andReturn(1); + expect(options.getMaxLoadedJsps()).andReturn(1); + replay(options); + context = new JspRuntimeContext(ctxt, options); + JspServletWrapper first = new JspServletWrapper(new StandardWrapper(), options, "/first.jsp", false, context); + context.addWrapper("/first.jsp", first); + context.push(first); + JspServletWrapper second = new JspServletWrapper(new StandardWrapper(), options, "/second.jsp", false, context); + context.addWrapper("/second.jsp", second); + context.push(second); + context.checkUnload(); + verify(options); + } +} Index: test/org/apache/jasper/util/JspQueueTest.java =================================================================== --- test/org/apache/jasper/util/JspQueueTest.java (revision 0) +++ test/org/apache/jasper/util/JspQueueTest.java (revision 0) @@ -0,0 +1,84 @@ +/** + * + */ +package org.apache.jasper.util; + +import junit.framework.TestCase; + +/** + * Queuing tests. The Queue is supposed to hold a set of instances in sorted order. + * Sorting order is determined by the instances' content. As this content may change + * during instance life, the Queue must be cheap to update - ideally in constant + * time. + * + * Access to the first element in the queue must happen in constant time. + * + * Only a minimal set of operations is implemented. + * + * @author isabel + */ +public class JspQueueTest extends TestCase { + + private JspQueue queue; + + protected void setUp() throws Exception { + super.setUp(); + queue = new JspQueue(); + } + + public void testEmpty() { + assertNull(queue.pop()); + } + + public void testInsertAndRemoveOne() { + String object = "first"; + queue.push(object); + assertSame("Polling should return the oldest object inserted.", object, queue.pop()); + } + + public void testInsertAndMoveEnd() { + String first = "first"; + String second = "second"; + String third = "third"; + queue.push(first); + queue.push(second); + queue.push(third); + assertSame("Polling should return oldest object inserted.", first, queue.pop()); + } + + public void testInsertAndMoveMiddle() { + String first = "first"; + String second = "second"; + String third = "third"; + Entry ticket1 = queue.push(first); + queue.push(second); + queue.push(third); + queue.makeYoungest(ticket1); + assertSame("Polling should return oldest object inserted.", second, queue.pop()); + } + + public void testInsertAndMoveLast() { + String first = "first"; + String second = "second"; + String third = "third"; + Entry ticket1 = queue.push(first); + Entry ticket2 = queue.push(second); + queue.push(third); + queue.makeYoungest(ticket2); + queue.makeYoungest(ticket1); + assertSame("Polling should return oldest object inserted.", third, queue.pop()); + } + + public void testInsertAndMoveBeginning() { + String first = "first"; + String second = "second"; + String third = "third"; + Entry ticket1 = queue.push(first); + Entry ticket2 = queue.push(second); + Entry ticket3 = queue.push(third); + queue.makeYoungest(ticket1); + queue.makeYoungest(ticket2); + queue.makeYoungest(ticket3); + assertSame("Polling should return oldest object inserted.", first, queue.pop()); + } +} Index: java/org/apache/jasper/JspC.java =================================================================== --- java/org/apache/jasper/JspC.java (revision 890701) +++ java/org/apache/jasper/JspC.java (working copy) @@ -447,6 +447,10 @@ return cache; } + public int getMaxLoadedJsps() { + return -1; + } + /** * Background compilation check intervals in seconds */ Index: java/org/apache/jasper/servlet/JspServlet.java =================================================================== --- java/org/apache/jasper/servlet/JspServlet.java (revision 890701) +++ java/org/apache/jasper/servlet/JspServlet.java (working copy) @@ -286,6 +286,7 @@ public void periodicEvent() { + rctxt.checkUnload(); rctxt.checkCompile(); } Index: java/org/apache/jasper/servlet/JspServletWrapper.java =================================================================== --- java/org/apache/jasper/servlet/JspServletWrapper.java (revision 890701) +++ java/org/apache/jasper/servlet/JspServletWrapper.java (working copy) @@ -40,6 +40,8 @@ import org.apache.jasper.compiler.Localizer; import org.apache.jasper.runtime.InstanceManagerFactory; import org.apache.jasper.runtime.JspSourceDependent; +import org.apache.jasper.util.Entry; +import org.apache.jasper.util.JspQueue; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.InstanceManager; @@ -81,6 +83,7 @@ private JasperException compileException; private long servletClassLastModifiedTime; private long lastModificationTest = 0L; + private Entry ticket; /* * JspServletWrapper for JSP pages. @@ -277,6 +280,10 @@ return tripCount--; } + public String getJspUri() { + return jspUri; + } + public void service(HttpServletRequest request, HttpServletResponse response, boolean precompile) @@ -310,6 +317,10 @@ // The following sets reload to true, if necessary ctxt.compile(); + + if (options.getDevelopment() && options.getMaxLoadedJsps() > 0) { + ctxt.getRuntimeContext().unloadJsp(); + } } } else { if (compileException != null) { @@ -371,7 +382,14 @@ } else { theServlet.service(request, response); } - + if (options.getMaxLoadedJsps() > 0) { + synchronized(this) { + if (ticket == null) + ticket = ctxt.getRuntimeContext().push(this); + else + ctxt.getRuntimeContext().makeFirst(ticket); + } + } } catch (UnavailableException ex) { String includeRequestUri = (String) request.getAttribute("javax.servlet.include.request_uri"); Index: java/org/apache/jasper/EmbeddedServletOptions.java =================================================================== --- java/org/apache/jasper/EmbeddedServletOptions.java (revision 890701) +++ java/org/apache/jasper/EmbeddedServletOptions.java (working copy) @@ -181,6 +181,12 @@ private boolean displaySourceFragment = true; + /** + * The maxim number of loaded jsps per web-application. If there are more + * jsps loaded, they will be unloaded. + */ + private int maxLoadedJsps = -1; + public String getProperty(String name ) { return settings.getProperty( name ); } @@ -371,6 +377,14 @@ } /** + * Should any jsps be unloaded? If set to a value greater than 0 eviction of jsps + * is started. Default: -1 + * */ + public int getMaxLoadedJsps() { + return maxLoadedJsps; + } + + /** * Create an EmbeddedServletOptions object using data available from * ServletConfig and ServletContext. */ @@ -639,6 +653,17 @@ } } + String maxLoadedJsps = config.getInitParameter("maxLoadedJsps"); + if (maxLoadedJsps != null) { + try { + this.maxLoadedJsps = Integer.parseInt(maxLoadedJsps); + } catch(NumberFormatException ex) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.maxLoadedJsps", ""+this.maxLoadedJsps)); + } + } + } + // Setup the global Tag Libraries location cache for this // web-application. tldLocationsCache = new TldLocationsCache(context); Index: java/org/apache/jasper/resources/LocalStrings.properties =================================================================== --- java/org/apache/jasper/resources/LocalStrings.properties (revision 890701) +++ java/org/apache/jasper/resources/LocalStrings.properties (working copy) @@ -176,6 +176,7 @@ jsp.warning.genchararray=Warning: Invalid value for the initParam genStrAsCharArray. Will use the default value of \"false\" jsp.warning.suppressSmap=Warning: Invalid value for the initParam suppressSmap. Will use the default value of \"false\" jsp.warning.displaySourceFragment=Warning: Invalid value for the initParam displaySourceFragment. Will use the default value of \"true\" +jsp.warning.maxLoadedJsps=Warning: Invalid value for the initParam maxLoadedJsps. Will use the default value of \"-1\" jsp.error.badtaglib=Unable to open taglibrary {0} : {1} jsp.error.badGetReader=Cannot create a reader when the stream is not buffered jsp.warning.unknown.element.in.taglib=Unknown element ({0}) in taglib Index: java/org/apache/jasper/compiler/JspRuntimeContext.java =================================================================== --- java/org/apache/jasper/compiler/JspRuntimeContext.java (revision 890701) +++ java/org/apache/jasper/compiler/JspRuntimeContext.java (working copy) @@ -40,9 +40,11 @@ import org.apache.jasper.runtime.JspFactoryImpl; import org.apache.jasper.security.SecurityClassLoad; import org.apache.jasper.servlet.JspServletWrapper; +import org.apache.jasper.util.JspQueue; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; + /** * Class for tracking JSP compile time file dependencies when the * &060;%@include file="..."%&062; directive is used. @@ -171,8 +173,12 @@ * Maps JSP pages to their JspServletWrapper's */ private Map jsps = new ConcurrentHashMap(); - + /** + * Keeps JSP pages ordered by last access. + */ + private JspQueue jspQueue = new JspQueue(); + // ------------------------------------------------------ Public Methods /** @@ -205,6 +211,30 @@ } /** + * Push a newly compiled JspServletWrapper into the queue at first + * execution of jsp. + * + * @param jsw Servlet wrapper for jsp. + * @return a ticket that can be pushed to front of queue at later execution times. + * */ + public org.apache.jasper.util.Entry push(JspServletWrapper jsw) { + synchronized (jspQueue) { + return jspQueue.push(jsw); + } + } + + /** + * Push ticket for JspServletWrapper to front of the queue. + * + * @param ticket the ticket for the jsp. + * */ + public void makeFirst(org.apache.jasper.util.Entry ticket) { + synchronized( jspQueue ) { + jspQueue.makeYoungest(ticket); + } + } + + /** * Returns the number of JSPs for which JspServletWrappers exist, i.e., * the number of JSPs that have been loaded into the webapp. * @@ -468,5 +498,48 @@ return new SecurityHolder(source, permissions); } + /** Returns a JspServletWrapper that should be destroyed. Default strategy: Least recently used. */ + public JspServletWrapper getJspForUnload(final int maxLoadedJsps) { + if( jsps.size() > maxLoadedJsps ) { + synchronized( jsps ) { + JspServletWrapper oldest; + synchronized( jspQueue) { + oldest = jspQueue.pop(); + } + if (oldest != null) { + removeWrapper(oldest.getJspUri()); + return oldest; + } + } + } + return null; + } + /** + * Method used by background thread to check if any JSP's should be destroyed. + * If JSP's to be unloaded are found, they will be destroyed. + * Uses the lastCheck time from background compiler to determine if it is time to unload JSP's. + */ + public void checkUnload() { + if (options.getMaxLoadedJsps() > 0) { + long now = System.currentTimeMillis(); + if (now > (lastCheck + (options.getCheckInterval() * 1000L))) { + while (unloadJsp()); + } + } + } + + /** + * Checks whether there is a jsp to unload, if one is found, it is destroyed. + * */ + public boolean unloadJsp() { + JspServletWrapper jsw = getJspForUnload(options.getMaxLoadedJsps()); + if( null != jsw ) { + synchronized(jsw) { + jsw.destroy(); + return true; + } + } + return false; + } } Index: java/org/apache/jasper/Options.java =================================================================== --- java/org/apache/jasper/Options.java (revision 890701) +++ java/org/apache/jasper/Options.java (working copy) @@ -194,4 +194,10 @@ */ public Map getCache(); + /** + * The maxim number of loaded jsps per web-application. If there are more + * jsps loaded, they will be unloaded. If unset or less than 0, no jsps + * are unloaded. + */ + public int getMaxLoadedJsps(); } Index: java/org/apache/jasper/util/JspQueue.java =================================================================== --- java/org/apache/jasper/util/JspQueue.java (revision 0) +++ java/org/apache/jasper/util/JspQueue.java (revision 0) @@ -0,0 +1,86 @@ +/** + * + */ +package org.apache.jasper.util; + +/** + * + * The JspQueue is supposed to hold a set of instances in sorted order. + * Sorting order is determined by the instances' content. As this content may change + * during instance lifetime, the Queue must be cheap to update - ideally in constant + * time. + * + * Access to the first element in the queue must happen in constant time. + * + * Only a minimal set of operations is implemented. + * + * @author isabel + */ +public class JspQueue { + + /** Head of the queue. */ + private Entry head; + /** Last element of the queue.*/ + private Entry last; + + /** Initialize empty queue. */ + public JspQueue() { + head = null; + last = null; + } + + /** + * Adds an object to the end of the queue and returns the entry created for + * said object. The entry can later be reused for moving the entry back to + * the front of the list. + * @param object the object to append to the end of the list. + * @return a ticket for use when the object should be moved back to the front. + * */ + public Entry push(final T object) { + Entry entry = new Entry(object); + if (head == null) { + head = last = entry; + } else { + last.setPrevious(entry); + entry.setNext(last); + last = entry; + } + + return entry; + } + + /** + * Removes the head of the queue and returns its content. + * @return the content of the head of the queue. + **/ + public T pop() { + T content = null; + if (head != null) { + content = head.getContent(); + if (head.getPrevious() != null) + head.getPrevious().setNext(null); + head = head.getPrevious(); + } + return content; + } + + /** + * Moves the candidate to the front of the queue. + * @param candidate the entry to move to the front of the queue. + * */ + public void makeYoungest(final Entry candidate) { + if (candidate.getPrevious() != null) { + Entry candidateNext = candidate.getNext(); + Entry candidatePrev = candidate.getPrevious(); + candidatePrev.setNext(candidateNext); + if (candidateNext != null) + candidateNext.setPrevious(candidatePrev); + else + head = candidatePrev; + candidate.setNext(last); + candidate.setPrevious(null); + last.setPrevious(candidate); + last = candidate; + } + } +} Index: java/org/apache/jasper/util/Entry.java =================================================================== --- java/org/apache/jasper/util/Entry.java (revision 0) +++ java/org/apache/jasper/util/Entry.java (revision 0) @@ -0,0 +1,48 @@ +/** + * + */ +package org.apache.jasper.util; + +/** + * Implementation of a list entry. It exposes links to previous and next + * elements on package level only. + * + * @author isabel + */ +public class Entry { + + /** The content this entry is valid for.*/ + private final T content; + /** Pointer to next element in queue.*/ + private Entry next; + /** Pointer to previous element in queue.*/ + private Entry previous; + + public Entry(T object) { + content = object; + } + + protected void setNext(final Entry next) { + this.next = next; + } + + protected void setPrevious(final Entry previous) { + this.previous = previous; + } + + public T getContent() { + return content; + } + + public Entry getPrevious() { + return previous; + } + + public Entry getNext() { + return next; + } + + public String toString() { + return content.toString(); + } +} Index: build.xml =================================================================== --- build.xml (revision 890701) +++ build.xml (working copy) @@ -866,6 +866,12 @@ + + + + + + @@ -891,7 +897,7 @@ - + Index: .classpath =================================================================== --- .classpath (revision 890701) +++ .classpath (working copy) @@ -7,6 +7,7 @@ + Index: build.properties.default =================================================================== --- build.properties.default (revision 890701) +++ build.properties.default (working copy) @@ -50,6 +50,12 @@ # Mirror, was used when there were problems with the main SF downloads site # base-sf.loc=http://sunet.dl.sourceforge.net +# ----- Easymock ---- +easymock.version=2.4 +easymock.loc=http://mirrors.ibiblio.org/pub/mirrors/maven2/org/easymock/easymock/${easymock.version}/easymock_{easymock.version}.jar +easymock.dir=${base.path}/easymock/ +easymock.jar=${easymock.dir}/easymock.${easymock.version}.jar + # ----- Commons Logging, version 1.1 or later ----- commons-logging-version=1.1.1 commons-logging-src.loc=${base-commons.loc}/logging/source/commons-logging-${commons-logging-version}-src.tar.gz