1//
2//  ========================================================================
3//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4//  ------------------------------------------------------------------------
5//  All rights reserved. This program and the accompanying materials
6//  are made available under the terms of the Eclipse Public License v1.0
7//  and Apache License v2.0 which accompanies this distribution.
8//
9//      The Eclipse Public License is available at
10//      http://www.eclipse.org/legal/epl-v10.html
11//
12//      The Apache License v2.0 is available at
13//      http://www.opensource.org/licenses/apache2.0.php
14//
15//  You may elect to redistribute this code under either of these licenses.
16//  ========================================================================
17//
18
19package org.eclipse.jetty.webapp;
20
21import java.io.File;
22import java.io.IOException;
23import java.net.URL;
24import java.net.URLClassLoader;
25import java.security.CodeSource;
26import java.security.PermissionCollection;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.Enumeration;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Locale;
33import java.util.Set;
34import java.util.StringTokenizer;
35
36import org.eclipse.jetty.util.StringUtil;
37import org.eclipse.jetty.util.log.Log;
38import org.eclipse.jetty.util.log.Logger;
39import org.eclipse.jetty.util.resource.Resource;
40import org.eclipse.jetty.util.resource.ResourceCollection;
41
42
43/* ------------------------------------------------------------ */
44/** ClassLoader for HttpContext.
45 * Specializes URLClassLoader with some utility and file mapping
46 * methods.
47 *
48 * This loader defaults to the 2.3 servlet spec behavior where non
49 * system classes are loaded from the classpath in preference to the
50 * parent loader.  Java2 compliant loading, where the parent loader
51 * always has priority, can be selected with the
52 * {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)}
53 * method and influenced with {@link WebAppContext#isServerClass(String)} and
54 * {@link WebAppContext#isSystemClass(String)}.
55 *
56 * If no parent class loader is provided, then the current thread
57 * context classloader will be used.  If that is null then the
58 * classloader that loaded this class is used as the parent.
59 *
60 */
61public class WebAppClassLoader extends URLClassLoader
62{
63    private static final Logger LOG = Log.getLogger(WebAppClassLoader.class);
64
65    private final Context _context;
66    private final ClassLoader _parent;
67    private final Set<String> _extensions=new HashSet<String>();
68    private String _name=String.valueOf(hashCode());
69
70    /* ------------------------------------------------------------ */
71    /** The Context in which the classloader operates.
72     */
73    public interface Context
74    {
75        /* ------------------------------------------------------------ */
76        /** Convert a URL or path to a Resource.
77         * The default implementation
78         * is a wrapper for {@link Resource#newResource(String)}.
79         * @param urlOrPath The URL or path to convert
80         * @return The Resource for the URL/path
81         * @throws IOException The Resource could not be created.
82         */
83        Resource newResource(String urlOrPath) throws IOException;
84
85        /* ------------------------------------------------------------ */
86        /**
87         * @return Returns the permissions.
88         */
89        PermissionCollection getPermissions();
90
91        /* ------------------------------------------------------------ */
92        /** Is the class a System Class.
93         * A System class is a class that is visible to a webapplication,
94         * but that cannot be overridden by the contents of WEB-INF/lib or
95         * WEB-INF/classes
96         * @param clazz The fully qualified name of the class.
97         * @return True if the class is a system class.
98         */
99        boolean isSystemClass(String clazz);
100
101        /* ------------------------------------------------------------ */
102        /** Is the class a Server Class.
103         * A Server class is a class that is part of the implementation of
104         * the server and is NIT visible to a webapplication. The web
105         * application may provide it's own implementation of the class,
106         * to be loaded from WEB-INF/lib or WEB-INF/classes
107         * @param clazz The fully qualified name of the class.
108         * @return True if the class is a server class.
109         */
110        boolean isServerClass(String clazz);
111
112        /* ------------------------------------------------------------ */
113        /**
114         * @return True if the classloader should delegate first to the parent
115         * classloader (standard java behaviour) or false if the classloader
116         * should first try to load from WEB-INF/lib or WEB-INF/classes (servlet
117         * spec recommendation).
118         */
119        boolean isParentLoaderPriority();
120
121        /* ------------------------------------------------------------ */
122        String getExtraClasspath();
123
124    }
125
126    /* ------------------------------------------------------------ */
127    /** Constructor.
128     */
129    public WebAppClassLoader(Context context)
130        throws IOException
131    {
132        this(null,context);
133    }
134
135    /* ------------------------------------------------------------ */
136    /** Constructor.
137     */
138    public WebAppClassLoader(ClassLoader parent, Context context)
139        throws IOException
140    {
141        super(new URL[]{},parent!=null?parent
142                :(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
143                        :(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
144                                :ClassLoader.getSystemClassLoader())));
145        _parent=getParent();
146        _context=context;
147        if (_parent==null)
148            throw new IllegalArgumentException("no parent classloader!");
149
150        _extensions.add(".jar");
151        _extensions.add(".zip");
152
153        // TODO remove this system property
154        String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
155        if(extensions!=null)
156        {
157            StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
158            while(tokenizer.hasMoreTokens())
159                _extensions.add(tokenizer.nextToken().trim());
160        }
161
162        if (context.getExtraClasspath()!=null)
163            addClassPath(context.getExtraClasspath());
164    }
165
166    /* ------------------------------------------------------------ */
167    /**
168     * @return the name of the classloader
169     */
170    public String getName()
171    {
172        return _name;
173    }
174
175    /* ------------------------------------------------------------ */
176    /**
177     * @param name the name of the classloader
178     */
179    public void setName(String name)
180    {
181        _name=name;
182    }
183
184
185    /* ------------------------------------------------------------ */
186    public Context getContext()
187    {
188        return _context;
189    }
190
191    /* ------------------------------------------------------------ */
192    /**
193     * @param resource Comma or semicolon separated path of filenames or URLs
194     * pointing to directories or jar files. Directories should end
195     * with '/'.
196     */
197    public void addClassPath(Resource resource)
198        throws IOException
199    {
200        if (resource instanceof ResourceCollection)
201        {
202            for (Resource r : ((ResourceCollection)resource).getResources())
203                addClassPath(r);
204        }
205        else
206        {
207            addClassPath(resource.toString());
208        }
209    }
210
211    /* ------------------------------------------------------------ */
212    /**
213     * @param classPath Comma or semicolon separated path of filenames or URLs
214     * pointing to directories or jar files. Directories should end
215     * with '/'.
216     */
217    public void addClassPath(String classPath)
218    	throws IOException
219    {
220        if (classPath == null)
221            return;
222
223        StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");
224        while (tokenizer.hasMoreTokens())
225        {
226            Resource resource= _context.newResource(tokenizer.nextToken().trim());
227            if (LOG.isDebugEnabled())
228                LOG.debug("Path resource=" + resource);
229
230            // Add the resource
231            if (resource.isDirectory() && resource instanceof ResourceCollection)
232                addClassPath(resource);
233            else
234            {
235                // Resolve file path if possible
236                File file= resource.getFile();
237                if (file != null)
238                {
239                    URL url= resource.getURL();
240                    addURL(url);
241                }
242                else if (resource.isDirectory())
243                    addURL(resource.getURL());
244                else
245                    throw new IllegalArgumentException("!file: "+resource);
246            }
247        }
248    }
249
250    /* ------------------------------------------------------------ */
251    /**
252     * @param file Checks if this file type can be added to the classpath.
253     */
254    private boolean isFileSupported(String file)
255    {
256        int dot = file.lastIndexOf('.');
257        return dot!=-1 && _extensions.contains(file.substring(dot));
258    }
259
260    /* ------------------------------------------------------------ */
261    /** Add elements to the class path for the context from the jar and zip files found
262     *  in the specified resource.
263     * @param lib the resource that contains the jar and/or zip files.
264     */
265    public void addJars(Resource lib)
266    {
267        if (lib.exists() && lib.isDirectory())
268        {
269            String[] files=lib.list();
270            for (int f=0;files!=null && f<files.length;f++)
271            {
272                try
273                {
274                    Resource fn=lib.addPath(files[f]);
275                    String fnlc=fn.getName().toLowerCase(Locale.ENGLISH);
276                    // don't check if this is a directory, see Bug 353165
277                    if (isFileSupported(fnlc))
278                    {
279                        String jar=fn.toString();
280                        jar=StringUtil.replace(jar, ",", "%2C");
281                        jar=StringUtil.replace(jar, ";", "%3B");
282                        addClassPath(jar);
283                    }
284                }
285                catch (Exception ex)
286                {
287                    LOG.warn(Log.EXCEPTION,ex);
288                }
289            }
290        }
291    }
292
293    /* ------------------------------------------------------------ */
294    public PermissionCollection getPermissions(CodeSource cs)
295    {
296        // TODO check CodeSource
297        PermissionCollection permissions=_context.getPermissions();
298        PermissionCollection pc= (permissions == null) ? super.getPermissions(cs) : permissions;
299        return pc;
300    }
301
302    /* ------------------------------------------------------------ */
303    public Enumeration<URL> getResources(String name) throws IOException
304    {
305        boolean system_class=_context.isSystemClass(name);
306        boolean server_class=_context.isServerClass(name);
307
308        List<URL> from_parent = toList(server_class?null:_parent.getResources(name));
309        List<URL> from_webapp = toList((system_class&&!from_parent.isEmpty())?null:this.findResources(name));
310
311        if (_context.isParentLoaderPriority())
312        {
313            from_parent.addAll(from_webapp);
314            return Collections.enumeration(from_parent);
315        }
316        from_webapp.addAll(from_parent);
317        return Collections.enumeration(from_webapp);
318    }
319
320    /* ------------------------------------------------------------ */
321    private List<URL> toList(Enumeration<URL> e)
322    {
323        if (e==null)
324            return new ArrayList<URL>();
325        return Collections.list(e);
326    }
327
328    /* ------------------------------------------------------------ */
329    /**
330     * Get a resource from the classloader
331     *
332     * NOTE: this method provides a convenience of hacking off a leading /
333     * should one be present. This is non-standard and it is recommended
334     * to not rely on this behavior
335     */
336    public URL getResource(String name)
337    {
338        URL url= null;
339        boolean tried_parent= false;
340        boolean system_class=_context.isSystemClass(name);
341        boolean server_class=_context.isServerClass(name);
342
343        if (system_class && server_class)
344            return null;
345
346        if (_parent!=null &&(_context.isParentLoaderPriority() || system_class ) && !server_class)
347        {
348            tried_parent= true;
349
350            if (_parent!=null)
351                url= _parent.getResource(name);
352        }
353
354        if (url == null)
355        {
356            url= this.findResource(name);
357
358            if (url == null && name.startsWith("/"))
359            {
360                if (LOG.isDebugEnabled())
361                    LOG.debug("HACK leading / off " + name);
362                url= this.findResource(name.substring(1));
363            }
364        }
365
366        if (url == null && !tried_parent && !server_class )
367        {
368            if (_parent!=null)
369                url= _parent.getResource(name);
370        }
371
372        if (url != null)
373            if (LOG.isDebugEnabled())
374                LOG.debug("getResource("+name+")=" + url);
375
376        return url;
377    }
378
379    /* ------------------------------------------------------------ */
380    @Override
381    public Class<?> loadClass(String name) throws ClassNotFoundException
382    {
383        return loadClass(name, false);
384    }
385
386    /* ------------------------------------------------------------ */
387    @Override
388    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
389    {
390        Class<?> c= findLoadedClass(name);
391        ClassNotFoundException ex= null;
392        boolean tried_parent= false;
393
394        boolean system_class=_context.isSystemClass(name);
395        boolean server_class=_context.isServerClass(name);
396
397        if (system_class && server_class)
398        {
399            return null;
400        }
401
402        if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class)
403        {
404            tried_parent= true;
405            try
406            {
407                c= _parent.loadClass(name);
408                if (LOG.isDebugEnabled())
409                    LOG.debug("loaded " + c);
410            }
411            catch (ClassNotFoundException e)
412            {
413                ex= e;
414            }
415        }
416
417        if (c == null)
418        {
419            try
420            {
421                c= this.findClass(name);
422            }
423            catch (ClassNotFoundException e)
424            {
425                ex= e;
426            }
427        }
428
429        if (c == null && _parent!=null && !tried_parent && !server_class )
430            c= _parent.loadClass(name);
431
432        if (c == null)
433            throw ex;
434
435        if (resolve)
436            resolveClass(c);
437
438        if (LOG.isDebugEnabled())
439            LOG.debug("loaded " + c+ " from "+c.getClassLoader());
440
441        return c;
442    }
443
444    /* ------------------------------------------------------------ */
445    public String toString()
446    {
447        return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode());
448    }
449}
450