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