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