001/* 002 * Copyright (C) 2014 Jörg Prante 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.xbib.elasticsearch.plugin.jdbc.classloader.uri; 017 018import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceEnumeration; 019import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceFinder; 020import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceHandle; 021import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceLocation; 022import org.xbib.elasticsearch.plugin.jdbc.classloader.directory.DirectoryResourceLocation; 023import org.xbib.elasticsearch.plugin.jdbc.classloader.jar.JarResourceLocation; 024 025import java.io.File; 026import java.io.FileNotFoundException; 027import java.io.IOException; 028import java.net.MalformedURLException; 029import java.net.URI; 030import java.net.URISyntaxException; 031import java.net.URL; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Enumeration; 035import java.util.LinkedHashMap; 036import java.util.LinkedHashSet; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.Map; 040import java.util.Set; 041import java.util.StringTokenizer; 042import java.util.jar.Attributes; 043import java.util.jar.Manifest; 044 045public class URIResourceFinder implements ResourceFinder { 046 047 private final Object lock = new Object(); 048 049 private final Set<URI> uris = new LinkedHashSet<URI>(); 050 051 private final Map<URI, ResourceLocation> classPath = new LinkedHashMap<URI, ResourceLocation>(); 052 053 private final Set<File> watchedFiles = new LinkedHashSet<File>(); 054 055 private boolean destroyed = false; 056 057 public URIResourceFinder() { 058 } 059 060 public void destroy() { 061 synchronized (lock) { 062 if (destroyed) { 063 return; 064 } 065 destroyed = true; 066 uris.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 Map<URI, ResourceLocation> path = getClassPath(); 080 for (Map.Entry<URI, ResourceLocation> entry : path.entrySet()) { 081 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 (Map.Entry<URI, ResourceLocation> entry : getClassPath().entrySet()) { 097 ResourceLocation resourceLocation = entry.getValue(); 098 ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName); 099 if (resourceHandle != null) { 100 return resourceHandle.getUrl(); 101 } 102 } 103 } 104 return null; 105 } 106 107 public Enumeration<URL> findResources(String resourceName) { 108 synchronized (lock) { 109 return new ResourceEnumeration(new ArrayList<ResourceLocation>(getClassPath().values()), resourceName); 110 } 111 } 112 113 public void addURI(URI uri) { 114 add(Arrays.asList(uri)); 115 } 116 117 public URI[] getURIs() { 118 synchronized (lock) { 119 return uris.toArray(new URI[uris.size()]); 120 } 121 } 122 123 /** 124 * Adds a list of uris to the end of this class loader. 125 * 126 * @param uris the URLs to add 127 */ 128 protected void add(List<URI> uris) { 129 synchronized (lock) { 130 if (destroyed) { 131 throw new IllegalStateException("UriResourceFinder has been destroyed"); 132 } 133 boolean shouldRebuild = this.uris.addAll(uris); 134 if (shouldRebuild) { 135 rebuildClassPath(); 136 } 137 } 138 } 139 140 private Map<URI, ResourceLocation> getClassPath() { 141 assert Thread.holdsLock(lock) : "This method can only be called while holding the lock"; 142 for (File file : watchedFiles) { 143 if (file.canRead()) { 144 rebuildClassPath(); 145 break; 146 } 147 } 148 return classPath; 149 } 150 151 /** 152 * Rebuilds the entire class path. This class is called when new URIs are 153 * added or one of the watched files becomes readable. This method will not 154 * open jar files again, but will add any new entries not alredy open to the 155 * class path. If any file based uri is does not exist, we will watch for 156 * that file to appear. 157 */ 158 private void rebuildClassPath() { 159 assert Thread.holdsLock(lock) : "This method can only be called while holding the lock"; 160 // copy all of the existing locations into a temp map and clear the class path 161 Map<URI, ResourceLocation> existingJarFiles = new LinkedHashMap<URI, ResourceLocation>(classPath); 162 classPath.clear(); 163 LinkedList<URI> locationStack = new LinkedList<URI>(uris); 164 try { 165 while (!locationStack.isEmpty()) { 166 URI uri = locationStack.removeFirst(); 167 if (classPath.containsKey(uri)) { 168 continue; 169 } 170 // Check is this URL has already been opened 171 ResourceLocation resourceLocation = existingJarFiles.remove(uri); 172 // If not opened, cache the uri and wrap it with a resource location 173 if (resourceLocation == null) { 174 try { 175 resourceLocation = createResourceLocation(uri.toURL(), cacheUri(uri)); 176 } catch (FileNotFoundException e) { 177 // if this is a file URL, the file doesn't exist yet... watch to see if it appears later 178 if ("file".equals(uri.getScheme())) { 179 File file = new File(uri.getPath()); 180 watchedFiles.add(file); 181 continue; 182 183 } 184 } catch (IOException ignored) { 185 // can't seem to open the file... this is most likely a bad jar file 186 // so don't keep a watch out for it because that would require lots of checking 187 // Dain: We may want to review this decision later 188 continue; 189 } catch (UnsupportedOperationException ex) { 190 // the protocol for the JAR file's URL is not supported. This can occur when 191 // the jar file is embedded in an EAR or CAR file. 192 continue; 193 } 194 } 195 try { 196 // add the jar to our class path 197 if (resourceLocation != null && resourceLocation.getCodeSource() != null) { 198 classPath.put(resourceLocation.getCodeSource().toURI(), resourceLocation); 199 } 200 } catch (URISyntaxException ex) { 201 // ignore 202 } 203 // push the manifest classpath on the stack (make sure to maintain the order) 204 List<URI> manifestClassPath = getManifestClassPath(resourceLocation); 205 locationStack.addAll(0, manifestClassPath); 206 } 207 } catch (Error e) { 208 destroy(); 209 throw e; 210 } 211 for (ResourceLocation resourceLocation : existingJarFiles.values()) { 212 resourceLocation.close(); 213 } 214 } 215 216 protected File cacheUri(URI uri) throws IOException { 217 if (!"file".equals(uri.getScheme())) { 218 // download the jar 219 throw new UnsupportedOperationException("Only local file jars are supported " + uri); 220 } 221 File file = new File(uri.getPath()); 222 if (!file.exists()) { 223 throw new FileNotFoundException(file.getAbsolutePath()); 224 } 225 if (!file.canRead()) { 226 throw new IOException("File is not readable: " + file.getAbsolutePath()); 227 } 228 return file; 229 } 230 231 protected ResourceLocation createResourceLocation(URL codeSource, File cacheFile) throws IOException { 232 if (!cacheFile.exists()) { 233 throw new FileNotFoundException(cacheFile.getAbsolutePath()); 234 } 235 if (!cacheFile.canRead()) { 236 throw new IOException("File is not readable: " + cacheFile.getAbsolutePath()); 237 } 238 return cacheFile.isDirectory() ? 239 // DirectoryResourceLocation will only return "file" URLs within this directory 240 // do not use the DirectoryResourceLocation for non file based uris 241 new DirectoryResourceLocation(cacheFile) : 242 new JarResourceLocation(codeSource, cacheFile); 243 } 244 245 private List<URI> getManifestClassPath(ResourceLocation resourceLocation) { 246 List<URI> classPathUrls = new LinkedList<URI>(); 247 try { 248 // get the manifest, if possible 249 Manifest manifest = resourceLocation.getManifest(); 250 if (manifest == null) { 251 // some locations don't have a manifest 252 return classPathUrls; 253 } 254 // get the class-path attribute, if possible 255 String manifestClassPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); 256 if (manifestClassPath == null) { 257 return classPathUrls; 258 } 259 // build the uris... 260 // the class-path attribute is space delimited 261 URL codeSource = resourceLocation.getCodeSource(); 262 for (StringTokenizer tokenizer = new StringTokenizer(manifestClassPath, " "); tokenizer.hasMoreTokens(); ) { 263 String entry = tokenizer.nextToken(); 264 try { 265 // the class path entry is relative to the resource location code source 266 URL entryUrl = new URL(codeSource, entry); 267 classPathUrls.add(entryUrl.toURI()); 268 } catch (MalformedURLException ignored) { 269 // most likely a poorly named entry 270 } catch (URISyntaxException ignored) { 271 // most likely a poorly named entry 272 } 273 } 274 return classPathUrls; 275 } catch (IOException ignored) { 276 // error opening the manifest 277 return classPathUrls; 278 } 279 } 280}