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 */ 017package org.apache.xbean.classloader; 018 019import java.io.File; 020import java.io.FileNotFoundException; 021import java.io.IOException; 022import java.net.MalformedURLException; 023import java.net.URISyntaxException; 024import java.net.URL; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collections; 028import java.util.Enumeration; 029import java.util.Iterator; 030import java.util.LinkedHashMap; 031import java.util.LinkedHashSet; 032import java.util.LinkedList; 033import java.util.List; 034import java.util.Map; 035import java.util.StringTokenizer; 036import java.util.jar.Attributes; 037import java.util.jar.JarFile; 038import java.util.jar.Manifest; 039 040/** 041 * @version $Rev: 776705 $ $Date: 2009-05-20 16:09:47 +0200 (Wed, 20 May 2009) $ 042 */ 043public 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}