001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.xbean.classloader;
018
019import java.io.IOException;
020import java.io.File;
021import java.net.URL;
022import java.net.URI;
023import java.security.AccessControlContext;
024import java.security.AccessController;
025import java.security.CodeSource;
026import java.security.PrivilegedAction;
027import java.security.PrivilegedExceptionAction;
028import java.security.PrivilegedActionException;
029import java.security.cert.Certificate;
030import java.util.Collection;
031import java.util.Enumeration;
032import java.util.jar.Attributes;
033import java.util.jar.Manifest;
034
035/**
036 * The JarFileClassLoader that loads classes and resources from a list of JarFiles.  This method is simmilar to URLClassLoader
037 * except it properly closes JarFiles when the classloader is destroyed so that the file read lock will be released, and
038 * the jar file can be modified and deleted.
039 * <p>
040 * Note: This implementation currently does not work reliably on windows, since the jar URL handler included with the Sun JavaVM
041 * holds a read lock on the JarFile, and this lock is not released when the jar url is dereferenced.  To fix this a
042 * replacement for the jar url handler must be written.
043 *
044 * @author Dain Sundstrom
045 * @version $Id: JarFileClassLoader.java 437551 2006-08-28 06:14:47Z adc $
046 * @since 2.0
047 */
048public class JarFileClassLoader extends MultiParentClassLoader {
049    private static final URL[] EMPTY_URLS = new URL[0];
050
051    private final UrlResourceFinder resourceFinder = new UrlResourceFinder();
052    private final AccessControlContext acc;
053
054    /**
055     * Creates a JarFileClassLoader that is a child of the system class loader.
056     * @param name the name of this class loader
057     * @param urls a list of URLs from which classes and resources should be loaded
058     */
059    public JarFileClassLoader(String name, URL[] urls) {
060        super(name, EMPTY_URLS);
061        this.acc = AccessController.getContext();
062        addURLs(urls);
063    }
064
065    /**
066     * Creates a JarFileClassLoader that is a child of the specified class loader.
067     * @param name the name of this class loader
068     * @param urls a list of URLs from which classes and resources should be loaded
069     * @param parent the parent of this class loader
070     */
071    public JarFileClassLoader(String name, URL[] urls, ClassLoader parent) {
072        super(name, EMPTY_URLS, parent);
073        this.acc = AccessController.getContext();
074        addURLs(urls);
075    }
076
077    public JarFileClassLoader(String name, URL[] urls, ClassLoader parent, boolean inverseClassLoading, String[] hiddenClasses, String[] nonOverridableClasses) {
078        super(name, EMPTY_URLS, parent, inverseClassLoading, hiddenClasses, nonOverridableClasses);
079        this.acc = AccessController.getContext();
080        addURLs(urls);
081    }
082
083    /**
084     * Creates a named class loader as a child of the specified parents.
085     * @param name the name of this class loader
086     * @param urls the urls from which this class loader will classes and resources
087     * @param parents the parents of this class loader
088     */
089    public JarFileClassLoader(String name, URL[] urls, ClassLoader[] parents) {
090        super(name, EMPTY_URLS, parents);
091        this.acc = AccessController.getContext();
092        addURLs(urls);
093    }
094
095    public JarFileClassLoader(String name, URL[] urls, ClassLoader[] parents, boolean inverseClassLoading, Collection hiddenClasses, Collection nonOverridableClasses) {
096        super(name, EMPTY_URLS, parents, inverseClassLoading, hiddenClasses, nonOverridableClasses);
097        this.acc = AccessController.getContext();
098        addURLs(urls);
099    }
100
101    public JarFileClassLoader(String name, URL[] urls, ClassLoader[] parents, boolean inverseClassLoading, String[] hiddenClasses, String[] nonOverridableClasses) {
102        super(name, EMPTY_URLS, parents, inverseClassLoading, hiddenClasses, nonOverridableClasses);
103        this.acc = AccessController.getContext();
104        addURLs(urls);
105    }
106
107    /**
108     * {@inheritDoc}
109     */
110    public URL[] getURLs() {
111        return resourceFinder.getUrls();
112    }
113
114    /**
115     * {@inheritDoc}
116     */
117    public void addURL(final URL url) {
118        AccessController.doPrivileged(new PrivilegedAction() {
119            public Object run() {
120                resourceFinder.addUrl(url);
121                return null;
122            }
123        }, acc);
124    }
125
126    /**
127     * Adds an array of urls to the end of this class loader.
128     * @param urls the URLs to add
129     */
130    protected void addURLs(final URL[] urls) {
131        AccessController.doPrivileged(new PrivilegedAction() {
132            public Object run() {
133                resourceFinder.addUrls(urls);
134                return null;
135            }
136        }, acc);
137    }
138
139    /**
140     * {@inheritDoc}
141     */
142    public void destroy() {
143        resourceFinder.destroy();
144        super.destroy();
145    }
146
147    /**
148     * {@inheritDoc}
149     */
150    public URL findResource(final String resourceName) {
151        return (URL) AccessController.doPrivileged(new PrivilegedAction() {
152            public Object run() {
153                return resourceFinder.findResource(resourceName);
154            }
155        }, acc);
156    }
157
158    /**
159     * {@inheritDoc}
160     */
161    public Enumeration findResources(final String resourceName) throws IOException {
162        // todo this is not right
163        // first get the resources from the parent classloaders
164        Enumeration parentResources = super.findResources(resourceName);
165
166        // get the classes from my urls
167        Enumeration myResources = (Enumeration) AccessController.doPrivileged(new PrivilegedAction() {
168            public Object run() {
169                return resourceFinder.findResources(resourceName);
170            }
171        }, acc);
172
173        // join the two together
174        Enumeration resources = new UnionEnumeration(parentResources, myResources);
175        return resources;
176    }
177
178    /**
179     * {@inheritDoc}
180     */
181    protected String findLibrary(String libraryName) {
182        // if the libraryName is actually a directory it is invalid
183        int pathEnd = libraryName.lastIndexOf('/');
184        if (pathEnd == libraryName.length() - 1) {
185            throw new IllegalArgumentException("libraryName ends with a '/' character: " + libraryName);
186        }
187
188        // get the name if the library file
189        final String resourceName;
190        if (pathEnd < 0) {
191            resourceName = System.mapLibraryName(libraryName);
192        } else {
193            String path = libraryName.substring(0, pathEnd + 1);
194            String file = libraryName.substring(pathEnd + 1);
195            resourceName = path + System.mapLibraryName(file);
196        }
197
198        // get a resource handle to the library
199        ResourceHandle resourceHandle = (ResourceHandle) AccessController.doPrivileged(new PrivilegedAction() {
200            public Object run() {
201                return resourceFinder.getResource(resourceName);
202            }
203        }, acc);
204
205        if (resourceHandle == null) {
206            return null;
207        }
208
209        // the library must be accessable on the file system
210        URL url = resourceHandle.getUrl();
211        if (!"file".equals(url.getProtocol())) {
212            return null;
213        }
214
215        String path = new File(URI.create(url.toString())).getPath();
216        return path;
217    }
218
219    /**
220     * {@inheritDoc}
221     */
222    protected Class findClass(final String className) throws ClassNotFoundException {
223        try {
224            return (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
225                public Object run() throws ClassNotFoundException {
226                    // first think check if we are allowed to define the package
227                    SecurityManager securityManager = System.getSecurityManager();
228                    if (securityManager != null) {
229                        String packageName;
230                        int packageEnd = className.lastIndexOf('.');
231                        if (packageEnd >= 0) {
232                            packageName = className.substring(0, packageEnd);
233                            securityManager.checkPackageDefinition(packageName);
234                        }
235                    }
236
237
238                    // convert the class name to a file name
239                    String resourceName = className.replace('.', '/') + ".class";
240
241                    // find the class file resource
242                    ResourceHandle resourceHandle = resourceFinder.getResource(resourceName);
243                    if (resourceHandle == null) {
244                        throw new ClassNotFoundException(className);
245                    }
246
247                    byte[] bytes;
248                    Manifest manifest;
249                    try {
250                        // get the bytes from the class file
251                        bytes = resourceHandle.getBytes();
252
253                        // get the manifest for defining the packages
254                        manifest = resourceHandle.getManifest();
255                    } catch (IOException e) {
256                        throw new ClassNotFoundException(className, e);
257                    }
258
259                    // get the certificates for the code source
260                    Certificate[] certificates = resourceHandle.getCertificates();
261
262                    // the code source url is used to define the package and as the security context for the class
263                    URL codeSourceUrl = resourceHandle.getCodeSourceUrl();
264
265                    // define the package (required for security)
266                    definePackage(className, codeSourceUrl, manifest);
267
268                    // this is the security context of the class
269                    CodeSource codeSource = new CodeSource(codeSourceUrl, certificates);
270
271                    // load the class into the vm
272                    Class clazz = defineClass(className, bytes, 0, bytes.length, codeSource);
273                    return clazz;
274                }
275            }, acc);
276        } catch (PrivilegedActionException e) {
277            throw (ClassNotFoundException) e.getException();
278        }
279    }
280
281    private void definePackage(String className, URL jarUrl, Manifest manifest) {
282        int packageEnd = className.lastIndexOf('.');
283        if (packageEnd < 0) {
284            return;
285        }
286
287        String packageName = className.substring(0, packageEnd);
288        String packagePath = packageName.replace('.', '/') + "/";
289
290        Attributes packageAttributes = null;
291        Attributes mainAttributes = null;
292        if (manifest != null) {
293            packageAttributes = manifest.getAttributes(packagePath);
294            mainAttributes = manifest.getMainAttributes();
295        }
296        Package pkg = getPackage(packageName);
297        if (pkg != null) {
298            if (pkg.isSealed()) {
299                if (!pkg.isSealed(jarUrl)) {
300                    throw new SecurityException("Package was already sealed with another URL: package=" + packageName + ", url=" + jarUrl);
301                }
302            } else {
303                if (isSealed(packageAttributes, mainAttributes)) {
304                    throw new SecurityException("Package was already been loaded and not sealed: package=" + packageName + ", url=" + jarUrl);
305                }
306            }
307        } else {
308            String specTitle = getAttribute(Attributes.Name.SPECIFICATION_TITLE, packageAttributes, mainAttributes);
309            String specVendor = getAttribute(Attributes.Name.SPECIFICATION_VENDOR, packageAttributes, mainAttributes);
310            String specVersion = getAttribute(Attributes.Name.SPECIFICATION_VERSION, packageAttributes, mainAttributes);
311            String implTitle = getAttribute(Attributes.Name.IMPLEMENTATION_TITLE, packageAttributes, mainAttributes);
312            String implVendor = getAttribute(Attributes.Name.IMPLEMENTATION_VENDOR, packageAttributes, mainAttributes);
313            String implVersion = getAttribute(Attributes.Name.IMPLEMENTATION_VERSION, packageAttributes, mainAttributes);
314
315            URL sealBase = null;
316            if (isSealed(packageAttributes, mainAttributes)) {
317                sealBase = jarUrl;
318            }
319
320            definePackage(packageName, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
321        }
322    }
323
324    private String getAttribute(Attributes.Name name, Attributes packageAttributes, Attributes mainAttributes) {
325        if (packageAttributes != null) {
326            String value = packageAttributes.getValue(name);
327            if (value != null) {
328                return value;
329            }
330        }
331        if (mainAttributes != null) {
332            return mainAttributes.getValue(name);
333        }
334        return null;
335    }
336
337    private boolean isSealed(Attributes packageAttributes, Attributes mainAttributes) {
338        String sealed = getAttribute(Attributes.Name.SEALED, packageAttributes, mainAttributes);
339        if (sealed == null) {
340            return false;
341        }
342        return "true".equalsIgnoreCase(sealed);
343    }
344}