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 }