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