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.CodeSource;
024import java.security.cert.Certificate;
025import java.util.Collection;
026import java.util.Enumeration;
027import java.util.jar.Attributes;
028import java.util.jar.Manifest;
029
030/**
031 * The JarFileClassLoader that loads classes and resources from a list of JarFiles.  This method is simmilar to URLClassLoader
032 * except it properly closes JarFiles when the classloader is destroyed so that the file read lock will be released, and
033 * the jar file can be modified and deleted.
034 * <p>
035 * Note: This implementation currently does not work reliably on windows, since the jar URL handler included with the Sun JavaVM
036 * holds a read lock on the JarFile, and this lock is not released when the jar url is dereferenced.  To fix this a
037 * replacement for the jar url handler must be written.
038 *
039 * @author Dain Sundstrom
040 * @version $Id: JarFileClassLoader.java 1539053 2013-11-05 16:43:36Z gawor $
041 * @since 2.0
042 */
043public class JarFileClassLoader extends MultiParentClassLoader {
044    private static final URL[] EMPTY_URLS = new URL[0];
045
046    private final UrlResourceFinder resourceFinder = new UrlResourceFinder();
047
048    /**
049     * Creates a JarFileClassLoader that is a child of the system class loader.
050     * @param name the name of this class loader
051     * @param urls a list of URLs from which classes and resources should be loaded
052     */
053    public JarFileClassLoader(String name, URL[] urls) {
054        super(name, EMPTY_URLS);
055        addURLs(urls);
056    }
057
058    /**
059     * Creates a JarFileClassLoader that is a child of the specified class loader.
060     * @param name the name of this class loader
061     * @param urls a list of URLs from which classes and resources should be loaded
062     * @param parent the parent of this class loader
063     */
064    public JarFileClassLoader(String name, URL[] urls, ClassLoader parent) {
065        super(name, EMPTY_URLS, parent);
066        addURLs(urls);
067    }
068
069    public JarFileClassLoader(String name, URL[] urls, ClassLoader parent, boolean inverseClassLoading, String[] hiddenClasses, String[] nonOverridableClasses) {
070        super(name, EMPTY_URLS, parent, inverseClassLoading, hiddenClasses, nonOverridableClasses);
071        addURLs(urls);
072    }
073
074    /**
075     * Creates a named class loader as a child of the specified parents.
076     * @param name the name of this class loader
077     * @param urls the urls from which this class loader will classes and resources
078     * @param parents the parents of this class loader
079     */
080    public JarFileClassLoader(String name, URL[] urls, ClassLoader[] parents) {
081        super(name, EMPTY_URLS, parents);
082        addURLs(urls);
083    }
084
085    public JarFileClassLoader(String name, URL[] urls, ClassLoader[] parents, boolean inverseClassLoading, Collection hiddenClasses, Collection nonOverridableClasses) {
086        super(name, EMPTY_URLS, parents, inverseClassLoading, hiddenClasses, nonOverridableClasses);
087        addURLs(urls);
088    }
089
090    public JarFileClassLoader(String name, URL[] urls, ClassLoader[] parents, boolean inverseClassLoading, String[] hiddenClasses, String[] nonOverridableClasses) {
091        super(name, EMPTY_URLS, parents, inverseClassLoading, hiddenClasses, nonOverridableClasses);
092        addURLs(urls);
093    }
094
095    /**
096     * {@inheritDoc}
097     */
098    public URL[] getURLs() {
099        return resourceFinder.getUrls();
100    }
101
102    /**
103     * {@inheritDoc}
104     */
105    public void addURL(final URL url) {
106        resourceFinder.addUrl(url);
107    }
108
109    /**
110     * Adds an array of urls to the end of this class loader.
111     * @param urls the URLs to add
112     */
113    protected void addURLs(final URL[] urls) {
114        resourceFinder.addUrls(urls);
115    }
116
117    /**
118     * {@inheritDoc}
119     */
120    public void destroy() {
121        resourceFinder.destroy();
122        super.destroy();
123    }
124
125    /**
126     * {@inheritDoc}
127     */
128    public URL findResource(final String resourceName) {
129        return resourceFinder.findResource(resourceName);
130    }
131
132    /**
133     * {@inheritDoc}
134     */
135    public Enumeration findResources(final String resourceName) throws IOException {
136        // todo this is not right
137        // first get the resources from the parent classloaders
138        Enumeration parentResources = super.findResources(resourceName);
139
140        // get the classes from my urls
141        Enumeration myResources = resourceFinder.findResources(resourceName);
142
143        // join the two together
144        Enumeration resources = new UnionEnumeration(parentResources, myResources);
145        return resources;
146    }
147
148    /**
149     * {@inheritDoc}
150     */
151    protected String findLibrary(String libraryName) {
152        // if the libraryName is actually a directory it is invalid
153        int pathEnd = libraryName.lastIndexOf('/');
154        if (pathEnd == libraryName.length() - 1) {
155            throw new IllegalArgumentException("libraryName ends with a '/' character: " + libraryName);
156        }
157
158        // get the name if the library file
159        final String resourceName;
160        if (pathEnd < 0) {
161            resourceName = System.mapLibraryName(libraryName);
162        } else {
163            String path = libraryName.substring(0, pathEnd + 1);
164            String file = libraryName.substring(pathEnd + 1);
165            resourceName = path + System.mapLibraryName(file);
166        }
167
168        // get a resource handle to the library
169        ResourceHandle resourceHandle = resourceFinder.getResource(resourceName);
170
171        if (resourceHandle == null) {
172            return null;
173        }
174
175        // the library must be accessable on the file system
176        URL url = resourceHandle.getUrl();
177        if (!"file".equals(url.getProtocol())) {
178            return null;
179        }
180
181        String path = new File(URI.create(url.toString())).getPath();
182        return path;
183    }
184
185    /**
186     * {@inheritDoc}
187     */
188    protected Class findClass(final String className) throws ClassNotFoundException {
189        // first think check if we are allowed to define the package
190        SecurityManager securityManager = System.getSecurityManager();
191        if (securityManager != null) {
192            String packageName;
193            int packageEnd = className.lastIndexOf('.');
194            if (packageEnd >= 0) {
195                packageName = className.substring(0, packageEnd);
196                securityManager.checkPackageDefinition(packageName);
197            }
198        }
199
200        // convert the class name to a file name
201        String resourceName = className.replace('.', '/') + ".class";
202
203        // find the class file resource
204        ResourceHandle resourceHandle = resourceFinder.getResource(resourceName);
205        if (resourceHandle == null) {
206            throw new ClassNotFoundException(className);
207        }
208
209        byte[] bytes;
210        Manifest manifest;
211        try {
212            // get the bytes from the class file
213            bytes = resourceHandle.getBytes();
214            
215            // get the manifest for defining the packages
216            manifest = resourceHandle.getManifest();
217        } catch (IOException e) {
218            throw new ClassNotFoundException(className, e);
219        }
220
221        // get the certificates for the code source
222        Certificate[] certificates = resourceHandle.getCertificates();
223        
224        // the code source url is used to define the package and as the security context for the class
225        URL codeSourceUrl = resourceHandle.getCodeSourceUrl();
226
227        // define the package (required for security)
228        definePackage(className, codeSourceUrl, manifest);
229
230        // this is the security context of the class
231        CodeSource codeSource = new CodeSource(codeSourceUrl, certificates);
232        
233        // load the class into the vm
234        Class clazz = defineClass(className, bytes, 0, bytes.length, codeSource);
235        return clazz;
236    }
237
238    private void definePackage(String className, URL jarUrl, Manifest manifest) {
239        int packageEnd = className.lastIndexOf('.');
240        if (packageEnd < 0) {
241            return;
242        }
243
244        String packageName = className.substring(0, packageEnd);
245        String packagePath = packageName.replace('.', '/') + "/";
246
247        Attributes packageAttributes = null;
248        Attributes mainAttributes = null;
249        if (manifest != null) {
250            packageAttributes = manifest.getAttributes(packagePath);
251            mainAttributes = manifest.getMainAttributes();
252        }
253        Package pkg = getPackage(packageName);
254        if (pkg != null) {
255            if (pkg.isSealed()) {
256                if (!pkg.isSealed(jarUrl)) {
257                    throw new SecurityException("Package was already sealed with another URL: package=" + packageName + ", url=" + jarUrl);
258                }
259            } else {
260                if (isSealed(packageAttributes, mainAttributes)) {
261                    throw new SecurityException("Package was already been loaded and not sealed: package=" + packageName + ", url=" + jarUrl);
262                }
263            }
264        } else {
265            String specTitle = getAttribute(Attributes.Name.SPECIFICATION_TITLE, packageAttributes, mainAttributes);
266            String specVendor = getAttribute(Attributes.Name.SPECIFICATION_VENDOR, packageAttributes, mainAttributes);
267            String specVersion = getAttribute(Attributes.Name.SPECIFICATION_VERSION, packageAttributes, mainAttributes);
268            String implTitle = getAttribute(Attributes.Name.IMPLEMENTATION_TITLE, packageAttributes, mainAttributes);
269            String implVendor = getAttribute(Attributes.Name.IMPLEMENTATION_VENDOR, packageAttributes, mainAttributes);
270            String implVersion = getAttribute(Attributes.Name.IMPLEMENTATION_VERSION, packageAttributes, mainAttributes);
271
272            URL sealBase = null;
273            if (isSealed(packageAttributes, mainAttributes)) {
274                sealBase = jarUrl;
275            }
276
277            definePackage(packageName, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
278        }
279    }
280
281    private String getAttribute(Attributes.Name name, Attributes packageAttributes, Attributes mainAttributes) {
282        if (packageAttributes != null) {
283            String value = packageAttributes.getValue(name);
284            if (value != null) {
285                return value;
286            }
287        }
288        if (mainAttributes != null) {
289            return mainAttributes.getValue(name);
290        }
291        return null;
292    }
293
294    private boolean isSealed(Attributes packageAttributes, Attributes mainAttributes) {
295        String sealed = getAttribute(Attributes.Name.SEALED, packageAttributes, mainAttributes);
296        if (sealed == null) {
297            return false;
298        }
299        return "true".equalsIgnoreCase(sealed);
300    }
301}