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 }