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 } 121 122 return classes; 123 124 } catch (IOException e) { 125 throw new ClassPathScanException(e); 126 127 } catch (ClassNotFoundException e) { 128 throw new ClassPathScanException(e); 129 } 130 } 131 132 /** 133 * Converts this resource name to a fully qualified class name. 134 * 135 * @param resourceName The resource name. 136 * @return The class name. 137 */ 138 private String toClassName(String resourceName) { 139 String nameWithDots = resourceName.replace("/", "."); 140 return nameWithDots.substring(0, (nameWithDots.length() - ".class".length())); 141 } 142 143 /** 144 * Finds the resources names present at this location and below on the classpath starting with this prefix and 145 * ending with this suffix. 146 */ 147 private Set<String> findResourceNames(Location location, ResourceFilter predicate) throws IOException { 148 149 Set<String> resourceNames = new TreeSet<String>(); 150 151 List<URL> locationsUrls = getLocationUrlsForPath(location); 152 for (URL locationUrl : locationsUrls) { 153 LOG.debug("scanning URL: {}", locationUrl.toExternalForm()); 154 155 UrlResolver urlResolver = createUrlResolver(locationUrl.getProtocol()); 156 URL resolvedUrl = urlResolver.toStandardJavaUrl(locationUrl); 157 158 String protocol = resolvedUrl.getProtocol(); 159 ClassPathLocationScanner classPathLocationScanner = createLocationScanner(protocol); 160 if (classPathLocationScanner == null) { 161 String scanRoot = UrlUtils.toFilePath(resolvedUrl); 162 LOG.warn("Unable to scan location: {} (unsupported protocol: {})", scanRoot, protocol); 163 } else { 164 Set<String> names = resourceNameCache.get(classPathLocationScanner).get(resolvedUrl); 165 if (names == null) { 166 names = classPathLocationScanner.findResourceNames(location.getPath(), resolvedUrl); 167 resourceNameCache.get(classPathLocationScanner).put(resolvedUrl, names); 168 } 169 resourceNames.addAll(names); 170 } 171 } 172 173 return filterResourceNames(resourceNames, predicate); 174 } 175 176 /** 177 * Gets the physical location urls for this logical path on the classpath. 178 * 179 * @param location The location on the classpath. 180 * @return The underlying physical URLs. 181 * @throws IOException when the lookup fails. 182 */ 183 private List<URL> getLocationUrlsForPath(Location location) throws IOException { 184 if (locationUrlCache.containsKey(location)) { 185 return locationUrlCache.get(location); 186 } 187 188 LOG.debug("determining location urls for {} using ClassLoader {} ...", location, classLoader); 189 190 List<URL> locationUrls = new ArrayList<URL>(); 191 192 if (classLoader.getClass().getName().startsWith("com.ibm")) { 193 // WebSphere 194 Enumeration<URL> urls = classLoader.getResources(location.toString()); 195 if (!urls.hasMoreElements()) { 196 LOG.warn("Unable to resolve location " + location); 197 } 198 while (urls.hasMoreElements()) { 199 URL url = urls.nextElement(); 200 locationUrls.add(new URL(URLDecoder.decode(url.toExternalForm(), "UTF-8"))); 201 } 202 } else { 203 Enumeration<URL> urls = classLoader.getResources(location.getPath()); 204 if (!urls.hasMoreElements()) { 205 LOG.warn("Unable to resolve location " + location); 206 } 207 208 while (urls.hasMoreElements()) { 209 locationUrls.add(urls.nextElement()); 210 } 211 } 212 213 locationUrlCache.put(location, locationUrls); 214 215 return locationUrls; 216 } 217 218 /** 219 * Creates an appropriate URL resolver scanner for this url protocol. 220 * 221 * @param protocol The protocol of the location url to scan. 222 * @return The url resolver for this protocol. 223 */ 224 private UrlResolver createUrlResolver(String protocol) { 225 if (new EnvironmentDetection(classLoader).isJBossVFSv2() && protocol.startsWith("vfs")) { 226 return new JBossVFSv2UrlResolver(); 227 } 228 229 return new DefaultUrlResolver(); 230 } 231 232 /** 233 * Creates an appropriate location scanner for this url protocol. 234 * 235 * @param protocol The protocol of the location url to scan. 236 * @return The location scanner or {@code null} if it could not be created. 237 */ 238 private ClassPathLocationScanner createLocationScanner(String protocol) { 239 if (locationScannerCache.containsKey(protocol)) { 240 return locationScannerCache.get(protocol); 241 } 242 243 if ("file".equals(protocol)) { 244 FileSystemClassPathLocationScanner locationScanner = new FileSystemClassPathLocationScanner(); 245 locationScannerCache.put(protocol, locationScanner); 246 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 247 return locationScanner; 248 } 249 250 if ("jar".equals(protocol) 251 || "zip".equals(protocol) //WebLogic 252 || "wsjar".equals(protocol) //WebSphere 253 ) { 254 JarFileClassPathLocationScanner locationScanner = new JarFileClassPathLocationScanner(); 255 locationScannerCache.put(protocol, locationScanner); 256 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 257 return locationScanner; 258 } 259 260 EnvironmentDetection featureDetector = new EnvironmentDetection(classLoader); 261 if (featureDetector.isJBossVFSv3() && "vfs".equals(protocol)) { 262 JBossVFSv3ClassPathLocationScanner locationScanner = new JBossVFSv3ClassPathLocationScanner(); 263 locationScannerCache.put(protocol, locationScanner); 264 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 265 return locationScanner; 266 } 267 if (featureDetector.isOsgi() && ( 268 "bundle".equals(protocol) // Felix 269 || "bundleresource".equals(protocol)) //Equinox 270 ) { 271 OsgiClassPathLocationScanner locationScanner = new OsgiClassPathLocationScanner(); 272 locationScannerCache.put(protocol, locationScanner); 273 resourceNameCache.put(locationScanner, new HashMap<URL, Set<String>>()); 274 return locationScanner; 275 } 276 277 return null; 278 } 279 280 /** 281 * Filters this list of resource names to only include the ones whose filename matches this prefix and this suffix. 282 */ 283 private Set<String> filterResourceNames(Set<String> resourceNames, ResourceFilter predicate) { 284 285 Set<String> filteredResourceNames = new TreeSet<String>(); 286 for (String resourceName : resourceNames) { 287 if (predicate.isMatch(resourceName)) { 288 filteredResourceNames.add(resourceName); 289 } 290 } 291 return filteredResourceNames; 292 } 293}