View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.struts2.jasper.compiler;
19  
20  import com.opensymphony.xwork2.util.logging.Logger;
21  import com.opensymphony.xwork2.util.logging.LoggerFactory;
22  import org.apache.struts2.jasper.Constants;
23  import org.apache.struts2.jasper.JspCompilationContext;
24  import org.apache.struts2.jasper.Options;
25  import org.apache.struts2.jasper.runtime.JspFactoryImpl;
26  import org.apache.struts2.jasper.security.SecurityClassLoad;
27  import org.apache.struts2.jasper.servlet.JspServletWrapper;
28  
29  import javax.servlet.ServletContext;
30  import javax.servlet.jsp.JspFactory;
31  import java.io.File;
32  import java.io.FileNotFoundException;
33  import java.io.FilePermission;
34  import java.net.URL;
35  import java.net.URLClassLoader;
36  import java.security.CodeSource;
37  import java.security.PermissionCollection;
38  import java.security.Policy;
39  import java.security.cert.Certificate;
40  import java.util.Collections;
41  import java.util.HashMap;
42  import java.util.Iterator;
43  import java.util.Map;
44  
45  /***
46   * Class for tracking JSP compile time file dependencies when the
47   * &060;%@include file="..."%&062; directive is used.
48   * <p/>
49   * A background thread periodically checks the files a JSP page
50   * is dependent upon.  If a dpendent file changes the JSP page
51   * which included it is recompiled.
52   * <p/>
53   * Only used if a web application context is a directory.
54   *
55   * @author Glenn L. Nielsen
56   * @version $Revision: 466606 $
57   */
58  public final class JspRuntimeContext implements Runnable {
59  
60      // Logger
61      private Logger log = LoggerFactory.getLogger(JspRuntimeContext.class);
62  
63      /*
64       * Counts how many times the webapp's JSPs have been reloaded.
65       */
66      private int jspReloadCount;
67  
68      /***
69       * Preload classes required at runtime by a JSP servlet so that
70       * we don't get a defineClassInPackage security exception.
71       */
72      static {
73          JspFactoryImpl factory = new JspFactoryImpl();
74          SecurityClassLoad.securityClassLoad(factory.getClass().getClassLoader());
75          JspFactory.setDefaultFactory(factory);
76      }
77  
78      // ----------------------------------------------------------- Constructors
79  
80      /***
81       * Create a JspRuntimeContext for a web application context.
82       * <p/>
83       * Loads in any previously generated dependencies from file.
84       *
85       * @param context ServletContext for web application
86       */
87      public JspRuntimeContext(ServletContext context, Options options) {
88  
89          this.context = context;
90          this.options = options;
91  
92          // Get the parent class loader
93          parentClassLoader =
94                  (URLClassLoader) Thread.currentThread().getContextClassLoader();
95          if (parentClassLoader == null) {
96              parentClassLoader =
97                      (URLClassLoader) this.getClass().getClassLoader();
98          }
99  
100         if (log.isDebugEnabled()) {
101             if (parentClassLoader != null) {
102                 log.debug(Localizer.getMessage("jsp.message.parent_class_loader_is",
103                         parentClassLoader.toString()));
104             } else {
105                 log.debug(Localizer.getMessage("jsp.message.parent_class_loader_is",
106                         "<none>"));
107             }
108         }
109 
110         initClassPath();
111 
112         if (context instanceof org.apache.struts2.jasper.servlet.JspCServletContext) {
113             return;
114         }
115 
116         if (System.getSecurityManager() != null) {
117             initSecurity();
118         }
119 
120         // If this web application context is running from a
121         // directory, start the background compilation thread
122         String appBase = context.getRealPath("/");
123         if (!options.getDevelopment()
124                 && appBase != null
125                 && options.getCheckInterval() > 0) {
126             if (appBase.endsWith(File.separator)) {
127                 appBase = appBase.substring(0, appBase.length() - 1);
128             }
129             String directory =
130                     appBase.substring(appBase.lastIndexOf(File.separator));
131             threadName = threadName + "[" + directory + "]";
132             threadStart();
133         }
134     }
135 
136     // ----------------------------------------------------- Instance Variables
137 
138     /***
139      * This web applications ServletContext
140      */
141     private ServletContext context;
142     private Options options;
143     private URLClassLoader parentClassLoader;
144     private PermissionCollection permissionCollection;
145     private CodeSource codeSource;
146     private String classpath;
147 
148     /***
149      * Maps JSP pages to their JspServletWrapper's
150      */
151     private Map jsps = Collections.synchronizedMap(new HashMap());
152 
153 
154     /***
155      * The background thread.
156      */
157     private Thread thread = null;
158 
159 
160     /***
161      * The background thread completion semaphore.
162      */
163     private boolean threadDone = false;
164 
165 
166     /***
167      * Name to register for the background thread.
168      */
169     private String threadName = "JspRuntimeContext";
170 
171     // ------------------------------------------------------ Public Methods
172 
173     /***
174      * Add a new JspServletWrapper.
175      *
176      * @param jspUri JSP URI
177      * @param jsw    Servlet wrapper for JSP
178      */
179     public void addWrapper(String jspUri, JspServletWrapper jsw) {
180         jsps.remove(jspUri);
181         jsps.put(jspUri, jsw);
182     }
183 
184     /***
185      * Get an already existing JspServletWrapper.
186      *
187      * @param jspUri JSP URI
188      * @return JspServletWrapper for JSP
189      */
190     public JspServletWrapper getWrapper(String jspUri) {
191         return (JspServletWrapper) jsps.get(jspUri);
192     }
193 
194     /***
195      * Remove a  JspServletWrapper.
196      *
197      * @param jspUri JSP URI of JspServletWrapper to remove
198      */
199     public void removeWrapper(String jspUri) {
200         jsps.remove(jspUri);
201     }
202 
203     /***
204      * Returns the number of JSPs for which JspServletWrappers exist, i.e.,
205      * the number of JSPs that have been loaded into the webapp.
206      *
207      * @return The number of JSPs that have been loaded into the webapp
208      */
209     public int getJspCount() {
210         return jsps.size();
211     }
212 
213     /***
214      * Get the SecurityManager Policy CodeSource for this web
215      * applicaiton context.
216      *
217      * @return CodeSource for JSP
218      */
219     public CodeSource getCodeSource() {
220         return codeSource;
221     }
222 
223     /***
224      * Get the parent URLClassLoader.
225      *
226      * @return URLClassLoader parent
227      */
228     public URLClassLoader getParentClassLoader() {
229         return parentClassLoader;
230     }
231 
232     /***
233      * Get the SecurityManager PermissionCollection for this
234      * web application context.
235      *
236      * @return PermissionCollection permissions
237      */
238     public PermissionCollection getPermissionCollection() {
239         return permissionCollection;
240     }
241 
242     /***
243      * Process a "destory" event for this web application context.
244      */
245     public void destroy() {
246         threadStop();
247 
248         Iterator servlets = jsps.values().iterator();
249         while (servlets.hasNext()) {
250             ((JspServletWrapper) servlets.next()).destroy();
251         }
252     }
253 
254     /***
255      * Increments the JSP reload counter.
256      */
257     public synchronized void incrementJspReloadCount() {
258         jspReloadCount++;
259     }
260 
261     /***
262      * Resets the JSP reload counter.
263      *
264      * @param count Value to which to reset the JSP reload counter
265      */
266     public synchronized void setJspReloadCount(int count) {
267         this.jspReloadCount = count;
268     }
269 
270     /***
271      * Gets the current value of the JSP reload counter.
272      *
273      * @return The current value of the JSP reload counter
274      */
275     public int getJspReloadCount() {
276         return jspReloadCount;
277     }
278 
279 
280     // -------------------------------------------------------- Private Methods
281 
282     /***
283      * Method used by background thread to check the JSP dependencies
284      * registered with this class for JSP's.
285      */
286     private void checkCompile() {
287         Object[] wrappers = jsps.values().toArray();
288         for (int i = 0; i < wrappers.length; i++) {
289             JspServletWrapper jsw = (JspServletWrapper) wrappers[i];
290             JspCompilationContext ctxt = jsw.getJspEngineContext();
291             // JspServletWrapper also synchronizes on this when
292             // it detects it has to do a reload
293             synchronized (jsw) {
294                 try {
295                     ctxt.compile();
296                 } catch (FileNotFoundException ex) {
297                     ctxt.incrementRemoved();
298                 } catch (Throwable t) {
299                     jsw.getServletContext().log("Background compile failed",
300                             t);
301                 }
302             }
303         }
304     }
305 
306     /***
307      * The classpath that is passed off to the Java compiler.
308      */
309     public String getClassPath() {
310         return classpath;
311     }
312 
313     /***
314      * Method used to initialize classpath for compiles.
315      */
316     private void initClassPath() {
317 
318         URL[] urls = parentClassLoader.getURLs();
319         StringBuffer cpath = new StringBuffer();
320         String sep = System.getProperty("path.separator");
321 
322         for (int i = 0; i < urls.length; i++) {
323             // Tomcat 4 can use URL's other than file URL's,
324             // a protocol other than file: will generate a
325             // bad file system path, so only add file:
326             // protocol URL's to the classpath.
327 
328             if (urls[i].getProtocol().equals("file")) {
329                 cpath.append((String) urls[i].getFile() + sep);
330             }
331         }
332 
333         cpath.append(options.getScratchDir() + sep);
334 
335         String cp = (String) context.getAttribute(Constants.SERVLET_CLASSPATH);
336         if (cp == null || cp.equals("")) {
337             cp = options.getClassPath();
338         }
339 
340         classpath = cpath.toString() + cp;
341 
342         if (log.isDebugEnabled()) {
343             log.debug("Compilation classpath initialized: " + getClassPath());
344         }
345     }
346 
347     /***
348      * Method used to initialize SecurityManager data.
349      */
350     private void initSecurity() {
351 
352         // Setup the PermissionCollection for this web app context
353         // based on the permissions configured for the root of the
354         // web app context directory, then add a file read permission
355         // for that directory.
356         Policy policy = Policy.getPolicy();
357         if (policy != null) {
358             try {
359                 // Get the permissions for the web app context
360                 String docBase = context.getRealPath("/");
361                 if (docBase == null) {
362                     docBase = options.getScratchDir().toString();
363                 }
364                 String codeBase = docBase;
365                 if (!codeBase.endsWith(File.separator)) {
366                     codeBase = codeBase + File.separator;
367                 }
368                 File contextDir = new File(codeBase);
369                 URL url = contextDir.getCanonicalFile().toURL();
370                 codeSource = new CodeSource(url, (Certificate[]) null);
371                 permissionCollection = policy.getPermissions(codeSource);
372 
373                 // Create a file read permission for web app context directory
374                 if (!docBase.endsWith(File.separator)) {
375                     permissionCollection.add
376                             (new FilePermission(docBase, "read"));
377                     docBase = docBase + File.separator;
378                 } else {
379                     permissionCollection.add
380                             (new FilePermission
381                                     (docBase.substring(0, docBase.length() - 1), "read"));
382                 }
383                 docBase = docBase + "-";
384                 permissionCollection.add(new FilePermission(docBase, "read"));
385 
386                 // Create a file read permission for web app tempdir (work)
387                 // directory
388                 String workDir = options.getScratchDir().toString();
389                 if (!workDir.endsWith(File.separator)) {
390                     permissionCollection.add
391                             (new FilePermission(workDir, "read"));
392                     workDir = workDir + File.separator;
393                 }
394                 workDir = workDir + "-";
395                 permissionCollection.add(new FilePermission(workDir, "read"));
396 
397                 // Allow the JSP to access org.apache.struts2.jasper.runtime.HttpJspBase
398                 permissionCollection.add(new RuntimePermission(
399                         "accessClassInPackage.org.apache.struts2.jasper.runtime"));
400 
401                 if (parentClassLoader instanceof URLClassLoader) {
402                     URL[] urls = parentClassLoader.getURLs();
403                     String jarUrl = null;
404                     String jndiUrl = null;
405                     for (int i = 0; i < urls.length; i++) {
406                         if (jndiUrl == null
407                                 && urls[i].toString().startsWith("jndi:")) {
408                             jndiUrl = urls[i].toString() + "-";
409                         }
410                         if (jarUrl == null
411                                 && urls[i].toString().startsWith("jar:jndi:")
412                                 ) {
413                             jarUrl = urls[i].toString();
414                             jarUrl = jarUrl.substring(0, jarUrl.length() - 2);
415                             jarUrl = jarUrl.substring(0,
416                                     jarUrl.lastIndexOf('/')) + "/-";
417                         }
418                     }
419                     if (jarUrl != null) {
420                         permissionCollection.add(
421                                 new FilePermission(jarUrl, "read"));
422                         permissionCollection.add(
423                                 new FilePermission(jarUrl.substring(4), "read"));
424                     }
425                     if (jndiUrl != null)
426                         permissionCollection.add(
427                                 new FilePermission(jndiUrl, "read"));
428                 }
429             } catch (Exception e) {
430                 context.log("Security Init for context failed", e);
431             }
432         }
433     }
434 
435 
436     // -------------------------------------------------------- Thread Support
437 
438     /***
439      * Start the background thread that will periodically check for
440      * changes to compile time included files in a JSP.
441      *
442      * @throws IllegalStateException if we should not be starting
443      *                               a background thread now
444      */
445     protected void threadStart() {
446 
447         // Has the background thread already been started?
448         if (thread != null) {
449             return;
450         }
451 
452         // Start the background thread
453         threadDone = false;
454         thread = new Thread(this, threadName);
455         thread.setDaemon(true);
456         thread.start();
457 
458     }
459 
460 
461     /***
462      * Stop the background thread that is periodically checking for
463      * changes to compile time included files in a JSP.
464      */
465     protected void threadStop() {
466 
467         if (thread == null) {
468             return;
469         }
470 
471         threadDone = true;
472         thread.interrupt();
473         try {
474             thread.join();
475         } catch (InterruptedException e) {
476             ;
477         }
478 
479         thread = null;
480 
481     }
482 
483     /***
484      * Sleep for the duration specified by the <code>checkInterval</code>
485      * property.
486      */
487     protected void threadSleep() {
488 
489         try {
490             Thread.sleep(options.getCheckInterval() * 1000L);
491         } catch (InterruptedException e) {
492             ;
493         }
494 
495     }
496 
497 
498     // ------------------------------------------------------ Background Thread
499 
500 
501     /***
502      * The background thread that checks for changes to files
503      * included by a JSP and flags that a recompile is required.
504      */
505     public void run() {
506 
507         // Loop until the termination semaphore is set
508         while (!threadDone) {
509 
510             // Wait for our check interval
511             threadSleep();
512 
513             // Check for included files which are newer than the
514             // JSP which uses them.
515             try {
516                 checkCompile();
517             } catch (Throwable t) {
518                 log.error("Exception checking if recompile needed: ", t);
519             }
520         }
521 
522     }
523 
524 }