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 }