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