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 java.io.IOException;
019import java.net.JarURLConnection;
020import java.net.URISyntaxException;
021import java.net.URL;
022import java.net.URLConnection;
023import java.util.Enumeration;
024import java.util.Set;
025import java.util.TreeSet;
026import java.util.jar.JarEntry;
027import java.util.jar.JarFile;
028
029/**
030 * ClassPathLocationScanner for jar files.
031 */
032public class JarFileClassPathLocationScanner implements ClassPathLocationScanner {
033  public Set<String> findResourceNames(String location, URL locationUrl) throws IOException {
034
035    try (JarFile jarFile = getJarFromUrl(locationUrl)) {
036      // For Tomcat and non-expanded WARs.
037      String prefix = jarFile.getName().toLowerCase().endsWith(".war") ? "WEB-INF/classes/" : "";
038      return findResourceNamesFromJarFile(jarFile, prefix, location);
039    }
040  }
041
042  /**
043   * Retrieves the Jar file represented by this URL.
044   *
045   * @param locationUrl The URL of the jar.
046   * @return The jar file.
047   * @throws IOException when the jar could not be resolved.
048   */
049  private JarFile getJarFromUrl(URL locationUrl) throws IOException {
050    URLConnection con = locationUrl.openConnection();
051    if (con instanceof JarURLConnection) {
052      // Should usually be the case for traditional JAR files.
053      JarURLConnection jarCon = (JarURLConnection) con;
054      jarCon.setUseCaches(false);
055      return jarCon.getJarFile();
056    }
057
058    // No JarURLConnection -> need to resort to URL file parsing.
059    // We'll assume URLs of the format "jar:path!/entry", with the protocol
060    // being arbitrary as long as following the entry format.
061    // We'll also handle paths with and without leading "file:" prefix.
062    String urlFile = locationUrl.getFile();
063
064    int separatorIndex = urlFile.indexOf("!/");
065    if (separatorIndex != -1) {
066      String jarFileUrl = urlFile.substring(0, separatorIndex);
067      if (jarFileUrl.startsWith("file:")) {
068        try {
069          return new JarFile(new URL(jarFileUrl).toURI().getSchemeSpecificPart());
070        } catch (URISyntaxException ex) {
071          // Fallback for URLs that are not valid URIs (should hardly ever happen).
072          return new JarFile(jarFileUrl.substring("file:".length()));
073        }
074      }
075      return new JarFile(jarFileUrl);
076    }
077
078    return new JarFile(urlFile);
079  }
080
081  /**
082   * Finds all the resource names contained in this directory within this jar file.
083   *
084   * @param jarFile  The jar file.
085   * @param prefix   The prefix to ignore within the jar file.
086   * @param location The location to look under.
087   * @return The resource names.
088   */
089  private Set<String> findResourceNamesFromJarFile(JarFile jarFile, String prefix, String location) {
090    String toScan = prefix + location + (location.endsWith("/") ? "" : "/");
091    Set<String> resourceNames = new TreeSet<>();
092
093    Enumeration<JarEntry> entries = jarFile.entries();
094    while (entries.hasMoreElements()) {
095      String entryName = entries.nextElement().getName();
096      if (entryName.startsWith(toScan)) {
097        resourceNames.add(entryName.substring(prefix.length()));
098      }
099    }
100
101    return resourceNames;
102  }
103}