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}