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 }