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