Open Source Repository

Home /jodd/jodd-3.3.2 | Repository Home


jodd/io/findfile/ClassFinder.java
// Copyright (c) 2003-2012, Jodd Team (jodd.org). All Rights Reserved.

package jodd.io.findfile;

import jodd.io.FileNameUtil;
import jodd.util.StringUtil;
import jodd.util.Wildcard;
import jodd.util.ArraysUtil;
import jodd.io.FileUtil;
import jodd.io.StreamUtil;
import jodd.io.ZipUtil;

import java.net.URL;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.util.Enumeration;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;

/**
 * Simple utility that scans <code>URL</code>s for classes.
 * Its purpose is to help scanning class paths for some classes.
 * Content of Jar files is also examined.
 @see ClassScanner
 */
public abstract class ClassFinder {

  private static final String CLASS_FILE_EXT = ".class";
  private static final String JAR_FILE_EXT = ".jar";

  // ---------------------------------------------------------------- excluded jars

  /**
   * Array of system jars that are excluded from the search.
   * By default it consists of java runtime libraries.
   */
  protected String[] systemJars = new String[] {
      "*/jre/lib/*.jar",
      "*/jre/lib/ext/*.jar",
      "*/tools.jar",
      "*/j2ee.jar"
  };
  /**
   * Array of excluded jars.
   */
  protected String[] excludedJars;
  /**
   * Array of jar file name patterns that are included in the search.
   * This rule is applied after the excluded rule.
   */
  protected String[] includedJars;
  /**
   * If set to <code>true</code> jars will be scanned using path wildcards.
   */
  protected boolean usePathWildcards;


  public String[] getSystemJars() {
    return systemJars;
  }

  public void setSystemJars(String[] systemJars) {
    this.systemJars = systemJars;
  }

  public String[] getExcludedJars() {
    return excludedJars;
  }

  public void setExcludedJars(String... excludedJars) {
    this.excludedJars = excludedJars;
  }

  public String[] getIncludedJars() {
    return includedJars;
  }

  public void setIncludedJars(String... includedJars) {
    this.includedJars = includedJars;
  }

  public boolean isUsePathWildcards() {
    return usePathWildcards;
  }

  public void setUsePathWildcards(boolean usePathWildcards) {
    this.usePathWildcards = usePathWildcards;
  }

  // ---------------------------------------------------------------- included packages

  protected String[] includedEntries;    // array of included name patterns
  protected String[] excludedEntries;    // array of excluded name patterns

  public String[] getIncludedEntries() {
    return includedEntries;
  }

  /**
   * Sets included set of names that will be considered during configuration,
   */
  public void setIncludedEntries(String... includedEntries) {
    this.includedEntries = includedEntries;
  }

  public String[] getExcludedEntries() {
    return excludedEntries;
  }

  /**
   * Sets excluded names that narrows included set of packages.
   */
  public void setExcludedEntries(String... excludedEntries) {
    this.excludedEntries = excludedEntries;
  }

  // ---------------------------------------------------------------- implementation

  /**
   * If set to <code>true</code> all files will be scanned and not only classes.
   */
  protected boolean includeResources;
  /**
   * If set to <code>true</code> exceptions for entry scans are ignored.
   */
  protected boolean ignoreException;


  public boolean isIncludeResources() {
    return includeResources;
  }

  public void setIncludeResources(boolean includeResources) {
    this.includeResources = includeResources;
  }


  public boolean isIgnoreException() {
    return ignoreException;
  }

  public void setIgnoreException(boolean ignoreException) {
    this.ignoreException = ignoreException;
  }

  // ---------------------------------------------------------------- scan

  /**
   * Scans several URLs. If (#ignoreExceptions} is set, exceptions
   * per one URL will be ignored and loops continues. 
   */
  protected void scanUrls(URL... urls) {
    for (URL path : urls) {
      scanUrl(path);
    }
  }
  
  /**
   * Scans single URL for classes and jar files.
   * Callback {@link #onEntry(EntryData)} is called on
   * each class name.
   */
  protected void scanUrl(URL url) {
    File file = FileUtil.toFile(url);
    if (file == null) {
      if (ignoreException == false) {
        throw new FindFileException("URL is not a valid file: " + url);
      }
    }
    scanPath(file);
  }


  protected void scanPaths(File... paths) {
    for (File path : paths) {
      scanPath(path);
    }
  }

  protected void scanPaths(String... paths) {
    for (String path : paths) {
      scanPath(path);
    }
  }
  
  protected void scanPath(String path) {
    scanPath(new File(path));
  }

  /**
   * Returns <code>true</code> if some JAR file has to be accepted.
   */
  protected boolean acceptJar(File jarFile) {
    String path = jarFile.getAbsolutePath();
    path = FileNameUtil.separatorsToUnix(path);

    if (systemJars != null) {
      int ndx = usePathWildcards ?
          Wildcard.matchPathOne(path, systemJars:
          Wildcard.matchOne(path, systemJars);
      if (ndx != -1) {
        return false;
      }
    }
    if (excludedJars != null) {
      int ndx = usePathWildcards ?
          Wildcard.matchPathOne(path, excludedJars:
          Wildcard.matchOne(path, excludedJars);
      if (ndx != -1) {
        return false;
      }
    }
    if (includedJars != null) {
      int ndx = usePathWildcards ?
          Wildcard.matchPathOne(path, includedJars:
          Wildcard.matchOne(path, includedJars);
      if (ndx == -1) {
        return false;
      }
    }
    return true;
  }
  
  /**
   * Scans single path.
   */
  protected void scanPath(File file) {
    String path = file.getAbsolutePath();

    if (StringUtil.endsWithIgnoreCase(path, JAR_FILE_EXT== true) {

      if (acceptJar(file== false) {
        return;
      }
      scanJarFile(file);
    else if (file.isDirectory() == true) {
      scanClassPath(file);
    }
  }

  // ---------------------------------------------------------------- internal

  /**
   * Scans classes inside single JAR archive. Archive is scanned as a zip file.
   @see #onEntry(EntryData)
   */
  protected void scanJarFile(File file) {
    ZipFile zipFile;
    try {
      zipFile = new ZipFile(file);
    catch (IOException ioex) {
      if (ignoreException == false) {
        throw new FindFileException("Unable to work with zip file: " + file.getName(), ioex);
      }
      return;
    }
    Enumeration entries = zipFile.entries();
    while (entries.hasMoreElements()) {
      ZipEntry zipEntry = (ZipEntryentries.nextElement();
      String zipEntryName = zipEntry.getName();
      try {
        if (StringUtil.endsWithIgnoreCase(zipEntryName, CLASS_FILE_EXT)) {
          String entryName = prepareEntryName(zipEntryName, true);
          EntryData entryData = new EntryData(entryName, zipFile, zipEntry);
          try {
            scanEntry(entryData);
          finally {
            entryData.closeInputStreamIfOpen();
          }
        else if (includeResources == true) {
          String entryName = prepareEntryName(zipEntryName, false);
          EntryData entryData = new EntryData(entryName, zipFile, zipEntry);
          try {
            scanEntry(entryData);
          finally {
            entryData.closeInputStreamIfOpen();
          }
        }
      catch (RuntimeException rex) {
        if (ignoreException == false) {
          ZipUtil.close(zipFile);
          throw rex;
        }
      }
    }
    ZipUtil.close(zipFile);
  }

  /**
   * Scans single classpath directory.
   @see #onEntry(EntryData)
   */
  protected void scanClassPath(File root) {
    String rootPath = root.getAbsolutePath();
    if (rootPath.endsWith(File.separator== false) {
      rootPath += File.separatorChar;
    }

    FindFile ff = new FindFile().setIncludeDirs(false).setRecursive(true).searchPath(rootPath);
    File file;
    while ((file = ff.nextFile()) != null) {
      String filePath = file.getAbsolutePath();
      try {
        if (StringUtil.endsWithIgnoreCase(filePath, CLASS_FILE_EXT)) {
          scanClassFile(filePath, rootPath, file, true);
        else if (includeResources == true) {
          scanClassFile(filePath, rootPath, file, false);
        }
      catch (RuntimeException rex) {
        if (ignoreException == false) {
          throw rex;
        }
      }
    }
  }

  protected void scanClassFile(String filePath, String rootPath, File file, boolean isClass) {
    if (StringUtil.startsWithIgnoreCase(filePath, rootPath== true) {
      String entryName = prepareEntryName(filePath.substring(rootPath.length()), isClass);
      EntryData entryData = new EntryData(entryName, file);
      try {
        scanEntry(entryData);
      finally {
        entryData.closeInputStreamIfOpen();
      }
    }
  }

  /**
   * Prepares resource and class names. For classes, it strips '.class' from the end and converts
   * all (back)slashes to dots. For resources, it replaces all backslashes to slashes.
   */
  protected String prepareEntryName(String name, boolean isClass) {
    String entryName = name;
    if (isClass) {
      entryName = name.substring(0, name.length() 6);    // 6 == ".class".length()
      entryName = StringUtil.replaceChar(entryName, '/''.');
      entryName = StringUtil.replaceChar(entryName, '\\''.');
    else {
      entryName = '/' + StringUtil.replaceChar(entryName, '\\''/');
    }
    return entryName;
  }

  /**
   * Returns <code>true</code> if some entry name has to be accepted.
   @see #prepareEntryName(String, boolean)
   @see #scanEntry(EntryData) 
   */
  protected boolean acceptEntry(String entryName) {
    if (excludedEntries != null) {
      if (Wildcard.matchOne(entryName, excludedEntries!= -1) {
        return false;
      }
    }
    if (includedEntries != null) {
      if (Wildcard.matchOne(entryName, includedEntries== -1) {
        return false;
      }
    }
    return true;
  }


  /**
   * If entry name is {@link #acceptEntry(String) accepted} invokes {@link #onEntry(EntryData)} a callback}.
   */
  protected void scanEntry(EntryData entryData) {
    if (acceptEntry(entryData.getName()) == false) {
      return;
    }
    try {
      onEntry(entryData);
    catch (Exception ex) {
      throw new FindFileException("Unable to scan entry: " + entryData, ex);
    }
  }


  // ---------------------------------------------------------------- callback

  /**
   * Called during classpath scanning when class or resource is found.
   <li>Class name is java-alike class name (pk1.pk2.class) that may be immediately used
   * for dynamic loading.
   <li>Resource name starts with '\' and represents either jar path (\pk1/pk2/res) or relative file path (\pk1\pk2\res).
     <code>InputStream</code> is provided by InputStreamProvider and opened lazy.
   * Once opened, input stream doesn't have to be closed - this is done by this class anyway.
   */
  protected abstract void onEntry(EntryData entryDatathrows Exception;

  // ---------------------------------------------------------------- utilities

  /**
   * Returns type signature bytes used for searching in class file.
   */
  protected byte[] getTypeSignatureBytes(Class type) {
    String name = 'L' + type.getName().replace('.''/'';';
    return name.getBytes();
  }

  /**
   * Returns <code>true</code> if class contains {@link #getTypeSignatureBytes(Class) type signature}.
   * It searches the class content for bytecode signature. This is the fastest way of finding if come
   * class uses some type. Please note that if signature exists it still doesn't means that class uses
   * it in expected way, therefore, class should be loaded to complete the scan.
   */
  protected boolean isTypeSignatureInUse(InputStream inputStream, byte[] bytes) {
    try {
      byte[] data = StreamUtil.readBytes(inputStream);
      int index = ArraysUtil.indexOf(data, bytes);
      return index != -1;
    catch (IOException ioex) {
      throw new FindFileException("Unable to read bytes from input stream.", ioex);
    }
  }


  // ---------------------------------------------------------------- provider

  /**
   * Provides input stream on demand. Input stream is not open until get().
   */
  protected static class EntryData {

    private final File file;
    private final ZipFile zipFile;
    private final ZipEntry zipEntry;
    private final String name;

    EntryData(String name, ZipFile zipFile, ZipEntry zipEntry) {
      this.name = name;
      this.zipFile = zipFile;
      this.zipEntry = zipEntry;
      this.file = null;
      inputStream = null;
    }
    EntryData(String name, File file) {
      this.name = name;
      this.file = file;
      this.zipEntry = null;
      this.zipFile = null;
      inputStream = null;
    }

    private InputStream inputStream;

    /**
     * Returns entry name.
     */
    public String getName() {
      return name;
    }

    /**
     * Returns <code>true</code> if archive.
     */
    public boolean isArchive() {
      return zipFile != null;
    }

    /**
     * Returns archive name or <code>null</code> if entry is not inside archived file.
     */
    public String getArchiveName() {
      if (zipFile != null) {
        return zipFile.getName()
      }
      return null;
    }

    /**
     * Opens zip entry or plain file and returns its input stream.
     */
    public InputStream openInputStream() {
      if (zipFile != null) {
        try {
          inputStream = zipFile.getInputStream(zipEntry);
          return inputStream;
        catch (IOException ioex) {
          throw new FindFileException("Unable to get input stream for zip file: '" + zipFile.getName()
              "', entry: '" + zipEntry.getName() "'." , ioex);
        }
      }
      try {
        inputStream = new FileInputStream(file);
        return inputStream;
      catch (FileNotFoundException fnfex) {
        throw new FindFileException("Unable to open file: " + file.getAbsolutePath(), fnfex);
      }
    }

    /**
     * Closes input stream if opened.
     */
    void closeInputStreamIfOpen() {
      if (inputStream == null) {
        return;
      }
      StreamUtil.close(inputStream);
      inputStream = null;
    }

    @Override
    public String toString() {
      return "EntryData{" + name + '\'' +'}';
    }
  }
}