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 package org.apache.catalina.loader;
18
19 import java.beans.PropertyChangeEvent;
20 import java.beans.PropertyChangeListener;
21 import java.beans.PropertyChangeSupport;
22 import java.io.File;
23 import java.io.FilePermission;
24 import java.io.IOException;
25 import java.lang.reflect.Constructor;
26 import java.net.URL;
27 import java.net.URLClassLoader;
28
29 import javax.management.ObjectName;
30 import javax.servlet.ServletContext;
31
32 import org.apache.catalina.Context;
33 import org.apache.catalina.Globals;
34 import org.apache.catalina.LifecycleException;
35 import org.apache.catalina.LifecycleState;
36 import org.apache.catalina.Loader;
37 import org.apache.catalina.util.LifecycleMBeanBase;
38 import org.apache.catalina.util.ToStringUtil;
39 import org.apache.juli.logging.Log;
40 import org.apache.juli.logging.LogFactory;
41 import org.apache.tomcat.util.ExceptionUtils;
42 import org.apache.tomcat.util.buf.UDecoder;
43 import org.apache.tomcat.util.compat.JreCompat;
44 import org.apache.tomcat.util.modeler.Registry;
45 import org.apache.tomcat.util.res.StringManager;
46
47 /**
48  * Classloader implementation which is specialized for handling web
49  * applications in the most efficient way, while being Catalina aware (all
50  * accesses to resources are made through
51  * {@link org.apache.catalina.WebResourceRoot}).
52  * This class loader supports detection of modified
53  * Java classes, which can be used to implement auto-reload support.
54  * <p>
55  * This class loader is configured via the Resources children of its Context
56  * prior to calling <code>start()</code>.  When a new class is required,
57  * these Resources will be consulted first to locate the class.  If it
58  * is not present, the system class loader will be used instead.
59  *
60  * @author Craig R. McClanahan
61  * @author Remy Maucherat
62  */

63 public class WebappLoader extends LifecycleMBeanBase
64     implements Loader, PropertyChangeListener {
65
66     private static final Log log = LogFactory.getLog(WebappLoader.class);
67
68     // ----------------------------------------------------------- Constructors
69
70     /**
71      * Construct a new WebappLoader. The parent class loader will be defined by
72      * {@link Context#getParentClassLoader()}.
73      */

74     public WebappLoader() {
75         this(null);
76     }
77
78
79     /**
80      * Construct a new WebappLoader with the specified class loader
81      * to be defined as the parent of the ClassLoader we ultimately create.
82      *
83      * @param parent The parent class loader
84      *
85      * @deprecated Use {@link Context#setParentClassLoader(ClassLoader)} to
86      *             specify the required class loader. This method will be
87      *             removed in Tomcat 10 onwards.
88      */

89     @Deprecated
90     public WebappLoader(ClassLoader parent) {
91         super();
92         this.parentClassLoader = parent;
93     }
94
95
96     // ----------------------------------------------------- Instance Variables
97
98     /**
99      * The class loader being managed by this Loader component.
100      */

101     private WebappClassLoaderBase classLoader = null;
102
103
104     /**
105      * The Context with which this Loader has been associated.
106      */

107     private Context context = null;
108
109
110     /**
111      * The "follow standard delegation model" flag that will be used to
112      * configure our ClassLoader.
113      */

114     private boolean delegate = false;
115
116
117     /**
118      * The Java class name of the ClassLoader implementation to be used.
119      * This class should extend WebappClassLoaderBase, otherwise, a different
120      * loader implementation must be used.
121      */

122     private String loaderClass = ParallelWebappClassLoader.class.getName();
123
124
125     /**
126      * The parent class loader of the class loader we will create.
127      */

128     private ClassLoader parentClassLoader = null;
129
130
131     /**
132      * The reloadable flag for this Loader.
133      */

134     private boolean reloadable = false;
135
136
137     /**
138      * The string manager for this package.
139      */

140     protected static final StringManager sm =
141         StringManager.getManager(Constants.Package);
142
143
144     /**
145      * The property change support for this component.
146      */

147     protected final PropertyChangeSupport support = new PropertyChangeSupport(this);
148
149
150     /**
151      * Classpath set in the loader.
152      */

153     private String classpath = null;
154
155
156     // ------------------------------------------------------------- Properties
157
158     /**
159      * Return the Java class loader to be used by this Container.
160      */

161     @Override
162     public ClassLoader getClassLoader() {
163         return classLoader;
164     }
165
166
167     @Override
168     public Context getContext() {
169         return context;
170     }
171
172
173     @Override
174     public void setContext(Context context) {
175
176         if (this.context == context) {
177             return;
178         }
179
180         if (getState().isAvailable()) {
181             throw new IllegalStateException(
182                     sm.getString("webappLoader.setContext.ise"));
183         }
184
185         // Deregister from the old Context (if any)
186         if (this.context != null) {
187             this.context.removePropertyChangeListener(this);
188         }
189
190         // Process this property change
191         Context oldContext = this.context;
192         this.context = context;
193         support.firePropertyChange("context", oldContext, this.context);
194
195         // Register with the new Container (if any)
196         if (this.context != null) {
197             setReloadable(this.context.getReloadable());
198             this.context.addPropertyChangeListener(this);
199         }
200     }
201
202
203     /**
204      * Return the "follow standard delegation model" flag used to configure
205      * our ClassLoader.
206      */

207     @Override
208     public boolean getDelegate() {
209         return this.delegate;
210     }
211
212
213     /**
214      * Set the "follow standard delegation model" flag used to configure
215      * our ClassLoader.
216      *
217      * @param delegate The new flag
218      */

219     @Override
220     public void setDelegate(boolean delegate) {
221         boolean oldDelegate = this.delegate;
222         this.delegate = delegate;
223         support.firePropertyChange("delegate", Boolean.valueOf(oldDelegate),
224                                    Boolean.valueOf(this.delegate));
225     }
226
227
228     /**
229      * @return the ClassLoader class name.
230      */

231     public String getLoaderClass() {
232         return this.loaderClass;
233     }
234
235
236     /**
237      * Set the ClassLoader class name.
238      *
239      * @param loaderClass The new ClassLoader class name
240      */

241     public void setLoaderClass(String loaderClass) {
242         this.loaderClass = loaderClass;
243     }
244
245
246     /**
247      * Return the reloadable flag for this Loader.
248      */

249     @Override
250     public boolean getReloadable() {
251         return this.reloadable;
252     }
253
254
255     /**
256      * Set the reloadable flag for this Loader.
257      *
258      * @param reloadable The new reloadable flag
259      */

260     @Override
261     public void setReloadable(boolean reloadable) {
262         // Process this property change
263         boolean oldReloadable = this.reloadable;
264         this.reloadable = reloadable;
265         support.firePropertyChange("reloadable",
266                                    Boolean.valueOf(oldReloadable),
267                                    Boolean.valueOf(this.reloadable));
268     }
269
270
271     // --------------------------------------------------------- Public Methods
272
273     /**
274      * Add a property change listener to this component.
275      *
276      * @param listener The listener to add
277      */

278     @Override
279     public void addPropertyChangeListener(PropertyChangeListener listener) {
280
281         support.addPropertyChangeListener(listener);
282
283     }
284
285
286     /**
287      * Execute a periodic task, such as reloading, etc. This method will be
288      * invoked inside the classloading context of this container. Unexpected
289      * throwables will be caught and logged.
290      */

291     @Override
292     public void backgroundProcess() {
293         if (reloadable && modified()) {
294             try {
295                 Thread.currentThread().setContextClassLoader
296                     (WebappLoader.class.getClassLoader());
297                 if (context != null) {
298                     context.reload();
299                 }
300             } finally {
301                 if (context != null && context.getLoader() != null) {
302                     Thread.currentThread().setContextClassLoader
303                         (context.getLoader().getClassLoader());
304                 }
305             }
306         }
307     }
308
309
310     public String[] getLoaderRepositories() {
311         if (classLoader == null) {
312             return new String[0];
313         }
314         URL[] urls = classLoader.getURLs();
315         String[] result = new String[urls.length];
316         for (int i = 0; i < urls.length; i++) {
317             result[i] = urls[i].toExternalForm();
318         }
319         return result;
320     }
321
322     public String getLoaderRepositoriesString() {
323         String repositories[]=getLoaderRepositories();
324         StringBuilder sb=new StringBuilder();
325         forint i=0; i<repositories.length ; i++ ) {
326             sb.append( repositories[i]).append(":");
327         }
328         return sb.toString();
329     }
330
331
332     /**
333      * Classpath, as set in org.apache.catalina.jsp_classpath context
334      * property
335      *
336      * @return The classpath
337      */

338     public String getClasspath() {
339         return classpath;
340     }
341
342
343     /**
344      * Has the internal repository associated with this Loader been modified,
345      * such that the loaded classes should be reloaded?
346      */

347     @Override
348     public boolean modified() {
349         return classLoader != null ? classLoader.modified() : false ;
350     }
351
352
353     /**
354      * Remove a property change listener from this component.
355      *
356      * @param listener The listener to remove
357      */

358     @Override
359     public void removePropertyChangeListener(PropertyChangeListener listener) {
360         support.removePropertyChangeListener(listener);
361     }
362
363
364     /**
365      * Return a String representation of this component.
366      */

367     @Override
368     public String toString() {
369         return ToStringUtil.toString(this, context);
370     }
371
372
373     /**
374      * Start associated {@link ClassLoader} and implement the requirements
375      * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
376      *
377      * @exception LifecycleException if this component detects a fatal error
378      *  that prevents this component from being used
379      */

380     @Override
381     protected void startInternal() throws LifecycleException {
382
383         if (log.isDebugEnabled())
384             log.debug(sm.getString("webappLoader.starting"));
385
386         if (context.getResources() == null) {
387             log.info(sm.getString("webappLoader.noResources", context));
388             setState(LifecycleState.STARTING);
389             return;
390         }
391
392         // Construct a class loader based on our current repositories list
393         try {
394
395             classLoader = createClassLoader();
396             classLoader.setResources(context.getResources());
397             classLoader.setDelegate(this.delegate);
398
399             // Configure our repositories
400             setClassPath();
401
402             setPermissions();
403
404             classLoader.start();
405
406             String contextName = context.getName();
407             if (!contextName.startsWith("/")) {
408                 contextName = "/" + contextName;
409             }
410             ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
411                     classLoader.getClass().getSimpleName() + ",host=" +
412                     context.getParent().getName() + ",context=" + contextName);
413             Registry.getRegistry(nullnull)
414                 .registerComponent(classLoader, cloname, null);
415
416         } catch (Throwable t) {
417             t = ExceptionUtils.unwrapInvocationTargetException(t);
418             ExceptionUtils.handleThrowable(t);
419             throw new LifecycleException(sm.getString("webappLoader.startError"), t);
420         }
421
422         setState(LifecycleState.STARTING);
423     }
424
425
426     /**
427      * Stop associated {@link ClassLoader} and implement the requirements
428      * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
429      *
430      * @exception LifecycleException if this component detects a fatal error
431      *  that prevents this component from being used
432      */

433     @Override
434     protected void stopInternal() throws LifecycleException {
435
436         if (log.isDebugEnabled())
437             log.debug(sm.getString("webappLoader.stopping"));
438
439         setState(LifecycleState.STOPPING);
440
441         // Remove context attributes as appropriate
442         ServletContext servletContext = context.getServletContext();
443         servletContext.removeAttribute(Globals.CLASS_PATH_ATTR);
444
445         // Throw away our current class loader if any
446         if (classLoader != null) {
447             try {
448                 classLoader.stop();
449             } finally {
450                 classLoader.destroy();
451             }
452
453             // classLoader must be non-null to have been registered
454             try {
455                 String contextName = context.getName();
456                 if (!contextName.startsWith("/")) {
457                     contextName = "/" + contextName;
458                 }
459                 ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
460                         classLoader.getClass().getSimpleName() + ",host=" +
461                         context.getParent().getName() + ",context=" + contextName);
462                 Registry.getRegistry(nullnull).unregisterComponent(cloname);
463             } catch (Exception e) {
464                 log.warn(sm.getString("webappLoader.stopError"), e);
465             }
466         }
467
468
469         classLoader = null;
470     }
471
472
473     // ----------------------------------------- PropertyChangeListener Methods
474
475
476     /**
477      * Process property change events from our associated Context.
478      *
479      * @param event The property change event that has occurred
480      */

481     @Override
482     public void propertyChange(PropertyChangeEvent event) {
483
484         // Validate the source of this event
485         if (!(event.getSource() instanceof Context))
486             return;
487
488         // Process a relevant property change
489         if (event.getPropertyName().equals("reloadable")) {
490             try {
491                 setReloadable
492                     ( ((Boolean) event.getNewValue()).booleanValue() );
493             } catch (NumberFormatException e) {
494                 log.error(sm.getString("webappLoader.reloadable",
495                                  event.getNewValue().toString()));
496             }
497         }
498     }
499
500
501     // ------------------------------------------------------- Private Methods
502
503     /**
504      * Create associated classLoader.
505      */

506     private WebappClassLoaderBase createClassLoader()
507         throws Exception {
508
509         Class<?> clazz = Class.forName(loaderClass);
510         WebappClassLoaderBase classLoader = null;
511
512         if (parentClassLoader == null) {
513             parentClassLoader = context.getParentClassLoader();
514         } else {
515             context.setParentClassLoader(parentClassLoader);
516         }
517         Class<?>[] argTypes = { ClassLoader.class };
518         Object[] args = { parentClassLoader };
519         Constructor<?> constr = clazz.getConstructor(argTypes);
520         classLoader = (WebappClassLoaderBase) constr.newInstance(args);
521
522         return classLoader;
523     }
524
525
526     /**
527      * Configure associated class loader permissions.
528      */

529     private void setPermissions() {
530
531         if (!Globals.IS_SECURITY_ENABLED)
532             return;
533         if (context == null)
534             return;
535
536         // Tell the class loader the root of the context
537         ServletContext servletContext = context.getServletContext();
538
539         // Assigning permissions for the work directory
540         File workDir =
541             (File) servletContext.getAttribute(ServletContext.TEMPDIR);
542         if (workDir != null) {
543             try {
544                 String workDirPath = workDir.getCanonicalPath();
545                 classLoader.addPermission
546                     (new FilePermission(workDirPath, "read,write"));
547                 classLoader.addPermission
548                     (new FilePermission(workDirPath + File.separator + "-",
549                                         "read,write,delete"));
550             } catch (IOException e) {
551                 // Ignore
552             }
553         }
554
555         for (URL url : context.getResources().getBaseUrls()) {
556            classLoader.addPermission(url);
557         }
558     }
559
560
561     /**
562      * Set the appropriate context attribute for our class path.  This
563      * is required only because Jasper depends on it.
564      */

565     private void setClassPath() {
566
567         // Validate our current state information
568         if (context == null)
569             return;
570         ServletContext servletContext = context.getServletContext();
571         if (servletContext == null)
572             return;
573
574         StringBuilder classpath = new StringBuilder();
575
576         // Assemble the class path information from our class loader chain
577         ClassLoader loader = getClassLoader();
578
579         if (delegate && loader != null) {
580             // Skip the webapp loader for now as delegation is enabled
581             loader = loader.getParent();
582         }
583
584         while (loader != null) {
585             if (!buildClassPath(classpath, loader)) {
586                 break;
587             }
588             loader = loader.getParent();
589         }
590
591         if (delegate) {
592             // Delegation was enabled, go back and add the webapp paths
593             loader = getClassLoader();
594             if (loader != null) {
595                 buildClassPath(classpath, loader);
596             }
597         }
598
599         this.classpath = classpath.toString();
600
601         // Store the assembled class path as a servlet context attribute
602         servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath);
603     }
604
605
606     private boolean buildClassPath(StringBuilder classpath, ClassLoader loader) {
607         if (loader instanceof URLClassLoader) {
608             URL repositories[] = ((URLClassLoader) loader).getURLs();
609                 for (int i = 0; i < repositories.length; i++) {
610                     String repository = repositories[i].toString();
611                     if (repository.startsWith("file://"))
612                         repository = UDecoder.URLDecode(repository.substring(7));
613                     else if (repository.startsWith("file:"))
614                         repository = UDecoder.URLDecode(repository.substring(5));
615                     else
616                         continue;
617                     if (repository == null)
618                         continue;
619                     if (classpath.length() > 0)
620                         classpath.append(File.pathSeparator);
621                     classpath.append(repository);
622                 }
623         } else if (loader == ClassLoader.getSystemClassLoader()){
624             // Java 9 onwards. The internal class loaders no longer extend
625             // URLCLassLoader
626             String cp = System.getProperty("java.class.path");
627             if (cp != null && cp.length() > 0) {
628                 if (classpath.length() > 0) {
629                     classpath.append(File.pathSeparator);
630                 }
631                 classpath.append(cp);
632             }
633             return false;
634         } else {
635             // Ignore Graal "unknown" classloader
636             if (!JreCompat.isGraalAvailable()) {
637                 log.info(sm.getString("webappLoader.unknownClassLoader", loader, loader.getClass()));
638             }
639             return false;
640         }
641         return true;
642     }
643
644     @Override
645     protected String getDomainInternal() {
646         return context.getDomain();
647     }
648
649
650     @Override
651     protected String getObjectNameKeyProperties() {
652
653         StringBuilder name = new StringBuilder("type=Loader");
654
655         name.append(",host=");
656         name.append(context.getParent().getName());
657
658         name.append(",context=");
659
660         String contextName = context.getName();
661         if (!contextName.startsWith("/")) {
662             name.append("/");
663         }
664         name.append(contextName);
665
666         return name.toString();
667     }
668 }
669