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