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}