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 }