001/** 002 * Copyright 2010-2016 Boxfuse GmbH 003 * <p/> 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 * <p/> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p/> 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.avaje.classpath.scanner.internal.scanner.classpath; 017 018import org.avaje.classpath.scanner.ClassFilter; 019import org.avaje.classpath.scanner.core.ClassPathScanException; 020import org.avaje.classpath.scanner.FilterResource; 021import org.avaje.classpath.scanner.core.Location; 022import org.avaje.classpath.scanner.Resource; 023import org.avaje.classpath.scanner.ResourceFilter; 024import org.avaje.classpath.scanner.internal.EnvironmentDetection; 025import org.avaje.classpath.scanner.internal.ResourceAndClassScanner; 026import org.avaje.classpath.scanner.internal.UrlUtils; 027import org.avaje.classpath.scanner.internal.scanner.classpath.jboss.JBossVFSv2UrlResolver; 028import org.avaje.classpath.scanner.internal.scanner.classpath.jboss.JBossVFSv3ClassPathLocationScanner; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031 032import java.io.IOException; 033import java.net.URL; 034import java.net.URLDecoder; 035import java.util.ArrayList; 036import java.util.Enumeration; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040import java.util.Set; 041import java.util.TreeSet; 042 043/** 044 * ClassPath scanner. 045 */ 046public class ClassPathScanner implements ResourceAndClassScanner { 047 048 private static final Logger LOG = LoggerFactory.getLogger(ClassPathScanner.class); 049 050 /** 051 * The ClassLoader for loading migrations on the classpath. 052 */ 053 private final ClassLoader classLoader; 054 055 /** 056 * Cache location lookups. 057 */ 058 private final Map<Location, List<URL>> locationUrlCache = new HashMap<Location, List<URL>>(); 059 060 /** 061 * Cache location scanners. 062 */ 063 private final Map<String, ClassPathLocationScanner> locationScannerCache = new HashMap<String, ClassPathLocationScanner>(); 064 065 /** 066 * Cache resource names. 067 */ 068 private final Map<ClassPathLocationScanner, Map<URL, Set<String>>> resourceNameCache = new HashMap<ClassPathLocationScanner, Map<URL, Set<String>>>(); 069 070 /** 071 * Creates a new Classpath scanner. 072 * 073 * @param classLoader The ClassLoader for loading migrations on the classpath. 074 */ 075 public ClassPathScanner(ClassLoader classLoader) { 076 this.classLoader = classLoader; 077 } 078 079 @Override 080 public List<Resource> scanForResources(Location path, ResourceFilter predicate) { 081 082 try { 083 List<Resource> resources = new ArrayList<Resource>(); 084 085 Set<String> resourceNames = findResourceNames(path, predicate); 086 for (String resourceName : resourceNames) { 087 resources.add(new ClassPathResource(resourceName, classLoader)); 088 LOG.trace("... found resource: {}", resourceName); 089 } 090 091 return resources; 092 093 } catch (IOException e) { 094 throw new ClassPathScanException(e); 095 } 096 } 097 098 @Override 099 public List<Class<?>> scanForClasses(Location location, ClassFilter predicate) { 100 101 try { 102 List<Class<?>> classes = new ArrayList<Class<?>>(); 103 104 Set<String> resourceNames = findResourceNames(location, FilterResource.bySuffix(".class")); 105 106 LOG.debug("scanning for classes at {} found {} resources to check", location, resourceNames.size()); 107 for (String resourceName : resourceNames) { 108 String className = toClassName(resourceName); 109 try { 110 Class<?> clazz = classLoader.loadClass(className); 111 if (predicate.isMatch(clazz)) { 112 classes.add(clazz); 113 LOG.trace("... matched class: {} ", className); 114 } 115 } catch (NoClassDefFoundError err) { 116 // This happens on class that inherits from an other class which are no longer in the classpath 117 // e.g. "public class MyTestRunner extends BlockJUnit4ClassRunner" and junit was in scope "provided" 118 LOG.debug("... class " + className + " could not be loaded and will be ignored.", err); 119 120 } catch (ClassNotFoundException err) { 121 // This happens on class that inherits from an other class which are no longer in the classpath 122 // e.g. "public class MyTestRunner extends BlockJUnit4ClassRunner" and junit was in scope "provided" 123 LOG.debug("... class " + className + " could not be loaded and will be ignored.", err); 124 } 125 } 126 127 return classes; 128 129 } catch (IOException e) { 130 throw new ClassPathScanException(e); 131 } 132 } 133 134 /** 135 * Converts this resource name to a fully qualified class name. 136 * 137 * @param resourceName The resource name. 138 * @return The class name. 139 */ 140 private String toClassName(String resourceName) { 141 String nameWithDots = resourceName.replace("/", "."); 142 return nameWithDots.substring(0, (nameWithDots.length() - ".class".length())); 143 } 144 145 /** 146 * Finds the resources names present at this location and below on the classpath starting with this prefix and 147 * ending with this suffix. 148 */ 149 private Set<String> findResourceNames(Location location, ResourceFilter predicate) throws IOException { 150 151 Set<String> resourceNames = new TreeSet<String>(); 152 153 List<URL> locationsUrls = getLocationUrlsForPath(location); 154 for (URL locationUrl : locationsUrls) { 155 LOG.debug("scanning URL: {}", locationUrl.toExternalForm()); 156 157 UrlResolver urlResolver = createUrlResolver(locationUrl.getProtocol()); 158 URL resolvedUrl = urlResolver.toStandardJavaUrl(locationUrl); 159 160 String protocol = resolvedUrl.getProtocol(); 161 ClassPathLocationScanner classPathLocationScanner = createLocationScanner(protocol); 162 if (classPathLocationScanner == null) { 163 String scanRoot = UrlUtils.toFilePath(resolvedUrl); 164 LOG.warn("Unable to scan location: {} (unsupported protocol: {})", scanRoot, protocol); 165 } else { 166 Set<String> names = resourceNameCache.get(classPathLocationScanner).get(resolvedUrl); 167 if (names == null) { 168 names = classPathLocationScanner.findResourceNames(location.getPath(), resolvedUrl); 169 resourceNameCache.get(classPathLocationScanner).put(resolvedUrl, names); 170 } 171 resourceNames.addAll(names); 172 } 173 } 174 175 return filterResourceNames(resourceNames, predicate); 176 } 177 178 /** 179 * Gets the physical location urls for this logical path on the classpath. 180 * 181 * @param location The location on the classpath. 182 * @return The underlying physical URLs. 183 * @throws IOException when the lookup fails. 184 */ 185 private List<URL> getLocationUrlsForPath(Location location) throws IOException { 186 if (locationUrlCache.containsKey(location)) { 187 return locationUrlCache.get(location); 188 } 189 190 LOG.debug("determining location urls for {} using ClassLoader {} ...", location, classLoader); 191 192 List<URL> locationUrls = new ArrayList<URL>(); 193 194 if (classLoader.getClass().getName().startsWith("com.ibm")) { 195 // WebSphere 196 Enumeration<URL> urls = classLoader.getResources(location.toString()); 197 if (!urls.hasMoreElements()) { 198 LOG.warn("Unable to resolve location " + location); 199 } 200 while (urls.hasMoreElements()) { 201 URL url = urls.nextElement(); 202 locationUrls.add(new URL(URLDecoder.decode(url.toExternalForm(), "UTF-8"))); 203 } 204 } else { 205 Enumeration<URL> urls = classLoader.getResources(location.getPath()); 206 if (!urls.hasMoreElements()) { 207 LOG.warn("Unable to resolve location " + location); 208 } 209 210 while (urls.hasMoreElements()) { 211 locationUrls.add(urls.nextElement()); 212 } 213 } 214 215 locationUrlCache.put(location, locationUrls); 216 217 return locationUrls; 218 } 219 220 /** 221 * Creates an appropriate URL resolver scanner for this url protocol. 222 * 223 * @param protocol The protocol of the location url to scan. 224 * @return The url resolver for this protocol. 225 */ 226 private UrlResolver createUrlResolver(String protocol) { 227 if (new EnvironmentDetection(classLoader).isJBossVFSv2() && protocol.startsWith("vfs")) { 228 return new JBossVFSv2UrlResolver(); 229 } 230 231 return new DefaultUrlResolver(); 232 } 233 234 /** 235 * Creates an appropriate location scanner for this url protocol. 236 * 237 * @param protocol The protocol of the location url to scan. 238 * @return The location scanner or {@code null} if it could not be created. 239 */ 240 private ClassPathLocationScanner createLocationScanner(String protocol) { 241 if (locationScannerCache.containsKey(protocol)) { 242 return locationScannerCache.get(protocol); 243 } 244 245 if ("file".equals(protocol)) { 246 FileSystemClassPathLocationScanner locationScanner = new FileSystemClassPathLocationScanner(); 247 locationScannerCache.put(protocol, locationScanner); 248 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 249 return locationScanner; 250 } 251 252 if ("jar".equals(protocol) 253 || "zip".equals(protocol) //WebLogic 254 || "wsjar".equals(protocol) //WebSphere 255 ) { 256 JarFileClassPathLocationScanner locationScanner = new JarFileClassPathLocationScanner(); 257 locationScannerCache.put(protocol, locationScanner); 258 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 259 return locationScanner; 260 } 261 262 EnvironmentDetection featureDetector = new EnvironmentDetection(classLoader); 263 if (featureDetector.isJBossVFSv3() && "vfs".equals(protocol)) { 264 JBossVFSv3ClassPathLocationScanner locationScanner = new JBossVFSv3ClassPathLocationScanner(); 265 locationScannerCache.put(protocol, locationScanner); 266 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 267 return locationScanner; 268 } 269 if (featureDetector.isOsgi() && ( 270 "bundle".equals(protocol) // Felix 271 || "bundleresource".equals(protocol)) //Equinox 272 ) { 273 OsgiClassPathLocationScanner locationScanner = new OsgiClassPathLocationScanner(); 274 locationScannerCache.put(protocol, locationScanner); 275 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 276 return locationScanner; 277 } 278 279 return null; 280 } 281 282 /** 283 * Filters this list of resource names to only include the ones whose filename matches this prefix and this suffix. 284 */ 285 private Set<String> filterResourceNames(Set<String> resourceNames, ResourceFilter predicate) { 286 287 Set<String> filteredResourceNames = new TreeSet<String>(); 288 for (String resourceName : resourceNames) { 289 if (predicate.isMatch(resourceName)) { 290 filteredResourceNames.add(resourceName); 291 } 292 } 293 return filteredResourceNames; 294 } 295}