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