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.File;
020    import java.io.FileNotFoundException;
021    import java.io.IOException;
022    import java.net.MalformedURLException;
023    import java.net.URISyntaxException;
024    import java.net.URL;
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collections;
028    import java.util.Enumeration;
029    import java.util.Iterator;
030    import java.util.LinkedHashMap;
031    import java.util.LinkedHashSet;
032    import java.util.LinkedList;
033    import java.util.List;
034    import java.util.Map;
035    import java.util.StringTokenizer;
036    import java.util.jar.Attributes;
037    import java.util.jar.JarFile;
038    import java.util.jar.Manifest;
039    
040    /**
041     * @version $Rev: 776705 $ $Date: 2009-05-20 10:09:47 -0400 (Wed, 20 May 2009) $
042     */
043    public class UrlResourceFinder implements ResourceFinder {
044        private final Object lock = new Object();
045    
046        private final LinkedHashSet urls = new LinkedHashSet();
047        private final LinkedHashMap classPath = new LinkedHashMap();
048        private final LinkedHashSet watchedFiles = new LinkedHashSet();
049    
050        private boolean destroyed = false;
051    
052        public UrlResourceFinder() {
053        }
054    
055        public UrlResourceFinder(URL[] urls) {
056            addUrls(urls);
057        }
058    
059        public void destroy() {
060            synchronized (lock) {
061                if (destroyed) {
062                    return;
063                }
064                destroyed = true;
065                urls.clear();
066                for (Iterator iterator = classPath.values().iterator(); iterator.hasNext();) {
067                    ResourceLocation resourceLocation = (ResourceLocation) iterator.next();
068                    resourceLocation.close();
069                }
070                classPath.clear();
071            }
072        }
073    
074        public ResourceHandle getResource(String resourceName) {
075            synchronized (lock) {
076                if (destroyed) {
077                    return null;
078                }
079                for (Iterator iterator = getClassPath().entrySet().iterator(); iterator.hasNext();) {
080                    Map.Entry entry = (Map.Entry) iterator.next();
081                    ResourceLocation resourceLocation = (ResourceLocation) entry.getValue();
082                    ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName);
083                    if (resourceHandle != null && !resourceHandle.isDirectory()) {
084                        return resourceHandle;
085                    }
086                }
087            }
088            return null;
089        }
090    
091        public URL findResource(String resourceName) {
092            synchronized (lock) {
093                if (destroyed) {
094                    return null;
095                }
096                for (Iterator iterator = getClassPath().entrySet().iterator(); iterator.hasNext();) {
097                    Map.Entry entry = (Map.Entry) iterator.next();
098                    ResourceLocation resourceLocation = (ResourceLocation) entry.getValue();
099                    ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName);
100                    if (resourceHandle != null) {
101                        return resourceHandle.getUrl();
102                    }
103                }
104            }
105            return null;
106        }
107    
108        public Enumeration findResources(String resourceName) {
109            synchronized (lock) {
110                return new ResourceEnumeration(new ArrayList(getClassPath().values()), resourceName);
111            }
112        }
113    
114        public void addUrl(URL url) {
115            addUrls(Collections.singletonList(url));
116        }
117    
118        public URL[] getUrls() {
119            synchronized (lock) {
120                return (URL[]) urls.toArray(new URL[urls.size()]);
121            }
122        }
123    
124        /**
125         * Adds an array of urls to the end of this class loader.
126         * @param urls the URLs to add
127         */
128        protected void addUrls(URL[] urls) {
129            addUrls(Arrays.asList(urls));
130        }
131    
132        /**
133         * Adds a list of urls to the end of this class loader.
134         * @param urls the URLs to add
135         */
136        protected void addUrls(List urls) {
137            synchronized (lock) {
138                if (destroyed) {
139                    throw new IllegalStateException("UrlResourceFinder has been destroyed");
140                }
141    
142                boolean shouldRebuild = this.urls.addAll(urls);
143                if (shouldRebuild) {
144                    rebuildClassPath();
145                }
146            }
147        }
148    
149        private LinkedHashMap getClassPath() {
150            assert Thread.holdsLock(lock): "This method can only be called while holding the lock";
151    
152            for (Iterator iterator = watchedFiles.iterator(); iterator.hasNext();) {
153                File file = (File) iterator.next();
154                if (file.canRead()) {
155                    rebuildClassPath();
156                    break;
157                }
158            }
159    
160            return classPath;
161        }
162    
163        /**
164         * Rebuilds the entire class path.  This class is called when new URLs are added or one of the watched files
165         * becomes readable.  This method will not open jar files again, but will add any new entries not alredy open
166         * to the class path.  If any file based url is does not exist, we will watch for that file to appear.
167         */
168        private void rebuildClassPath() {
169            assert Thread.holdsLock(lock): "This method can only be called while holding the lock";
170    
171            // copy all of the existing locations into a temp map and clear the class path
172            Map existingJarFiles = new LinkedHashMap(classPath);
173            classPath.clear();
174    
175            LinkedList locationStack = new LinkedList(urls);
176            try {
177                while (!locationStack.isEmpty()) {
178                    URL url = (URL) locationStack.removeFirst();
179    
180                    // Skip any duplicate urls in the claspath
181                    if (classPath.containsKey(url)) {
182                        continue;
183                    }
184    
185                    // Check is this URL has already been opened
186                    ResourceLocation resourceLocation = (ResourceLocation) existingJarFiles.remove(url);
187    
188                    // If not opened, cache the url and wrap it with a resource location
189                    if (resourceLocation == null) {
190                        try {
191                            File file = cacheUrl(url);
192                            resourceLocation = createResourceLocation(url, file);
193                        } catch (FileNotFoundException e) {
194                            // if this is a file URL, the file doesn't exist yet... watch to see if it appears later
195                            if ("file".equals(url.getProtocol())) {
196                                File file = new File(url.getPath());
197                                watchedFiles.add(file);
198                                continue;
199    
200                            }
201                        } catch (IOException ignored) {
202                            // can't seem to open the file... this is most likely a bad jar file
203                            // so don't keep a watch out for it because that would require lots of checking
204                            // Dain: We may want to review this decision later
205                            continue;
206                        }
207                    }
208    
209                    // add the jar to our class path
210                    classPath.put(resourceLocation.getCodeSource(), resourceLocation);
211    
212                    // push the manifest classpath on the stack (make sure to maintain the order)
213                    List manifestClassPath = getManifestClassPath(resourceLocation);
214                    locationStack.addAll(0, manifestClassPath);
215                }
216            } catch (Error e) {
217                destroy();
218                throw e;
219            }
220    
221            for (Iterator iterator = existingJarFiles.values().iterator(); iterator.hasNext();) {
222                ResourceLocation resourceLocation = (ResourceLocation) iterator.next();
223                resourceLocation.close();
224            }
225        }
226    
227        protected File cacheUrl(URL url) throws IOException {
228            if (!"file".equals(url.getProtocol())) {
229                // download the jar
230                throw new Error("Only local file jars are supported " + url);
231            }
232    
233            File file;
234            try {
235                file = new File(url.toURI());
236            } catch (URISyntaxException e) {
237                file = new File(url.getPath());
238            }
239            if (!file.exists()) {
240                throw new FileNotFoundException(file.getAbsolutePath());
241            }
242            if (!file.canRead()) {
243                throw new IOException("File is not readable: " + file.getAbsolutePath());
244            }
245            return file;
246        }
247    
248        protected ResourceLocation createResourceLocation(URL codeSource, File cacheFile) throws IOException {
249            if (!cacheFile.exists()) {
250                throw new FileNotFoundException(cacheFile.getAbsolutePath());
251            }
252            if (!cacheFile.canRead()) {
253                throw new IOException("File is not readable: " + cacheFile.getAbsolutePath());
254            }
255    
256            ResourceLocation resourceLocation = null;
257            if (cacheFile.isDirectory()) {
258                // DirectoryResourceLocation will only return "file" URLs within this directory
259                // do not user the DirectoryResourceLocation for non file based urls
260                resourceLocation = new DirectoryResourceLocation(cacheFile);
261            } else {
262                resourceLocation = new JarResourceLocation(codeSource, new JarFile(cacheFile));
263            }
264            return resourceLocation;
265        }
266    
267        private List getManifestClassPath(ResourceLocation resourceLocation) {
268            try {
269                // get the manifest, if possible
270                Manifest manifest = resourceLocation.getManifest();
271                if (manifest == null) {
272                    // some locations don't have a manifest
273                    return Collections.EMPTY_LIST;
274                }
275    
276                // get the class-path attribute, if possible
277                String manifestClassPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
278                if (manifestClassPath == null) {
279                    return Collections.EMPTY_LIST;
280                }
281    
282                // build the urls...
283                // the class-path attribute is space delimited
284                URL codeSource = resourceLocation.getCodeSource();
285                LinkedList classPathUrls = new LinkedList();
286                for (StringTokenizer tokenizer = new StringTokenizer(manifestClassPath, " "); tokenizer.hasMoreTokens();) {
287                    String entry = tokenizer.nextToken();
288                    try {
289                        // the class path entry is relative to the resource location code source
290                        URL entryUrl = new URL(codeSource, entry);
291                        classPathUrls.addLast(entryUrl);
292                    } catch (MalformedURLException ignored) {
293                        // most likely a poorly named entry
294                    }
295                }
296                return classPathUrls;
297            } catch (IOException ignored) {
298                // error opening the manifest
299                return Collections.EMPTY_LIST;
300            }
301        }
302    }