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     */
017    package org.apache.xbean.classloader;
018    
019    import java.io.IOException;
020    import java.io.File;
021    import java.net.URL;
022    import java.net.URI;
023    import java.security.AccessControlContext;
024    import java.security.AccessController;
025    import java.security.CodeSource;
026    import java.security.PrivilegedAction;
027    import java.security.PrivilegedExceptionAction;
028    import java.security.PrivilegedActionException;
029    import java.security.cert.Certificate;
030    import java.util.Collection;
031    import java.util.Enumeration;
032    import java.util.jar.Attributes;
033    import 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     */
048    public 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    }