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 */
017package org.apache.xbean.classloader;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.net.MalformedURLException;
023import java.net.URISyntaxException;
024import java.net.URL;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.Enumeration;
029import java.util.Iterator;
030import java.util.LinkedHashMap;
031import java.util.LinkedHashSet;
032import java.util.LinkedList;
033import java.util.List;
034import java.util.Map;
035import java.util.StringTokenizer;
036import java.util.jar.Attributes;
037import java.util.jar.JarFile;
038import java.util.jar.Manifest;
039
040/**
041 * @version $Rev: 776705 $ $Date: 2009-05-20 16:09:47 +0200 (mer. 20 mai 2009) $
042 */
043public 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}