ClassPathHelper.java

/*
 * JPPF.
 * Copyright (C) 2005-2019 JPPF Team.
 * http://www.jppf.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jppf.example.extendedclassloading;

import java.io.*;
import java.net.URL;
import java.security.MessageDigest;
import java.util.regex.Pattern;

import org.jppf.utils.*;
import org.jppf.utils.streams.StreamUtils;

/**
 * A collection of helper methods to manage a repository of Java libraries
 * that are added dynamically to the node's classpath and downloaded from
 * the client when necessary.
 * @author Laurent Cohen
 */
public class ClassPathHelper {
  /**
   * Name of the job metadata property that holds the {@link ClassPath} definition for a job.
   */
  public static final String JOB_CLASSPATH = "job.class.path";
  /**
   * Name of the job metadata property that holds the {@link RepositoryFilter} for the libraries ot delete.
   */
  public static final String REPOSITORY_DELETE_FILTER = "repository.delete.filter";
  /**
   * The name of the algorithm used to generate the file signatures, such as MD5 or SHA-256.
   */
  public static final String SIGNATURE_ALGORITHM = "MD5";

  /**
   * Compute a signature for the specified file.
   * @param filename the file path.
   * @return a hexadecimal representation of the signature.
   */
  public static String computeSignature(final String filename) {
    try {
      return computeSignature(FileUtils.getFileInputStream(filename));
    } catch (@SuppressWarnings("unused") final Exception e) {
      return null;
    }
  }

  /**
   * Compute a signature for the specified file.
   * @param file the file path.
   * @return a hexadecimal representation of the signature.
   */
  public static String computeSignature(final File file) {
    try {
      return computeSignature(new BufferedInputStream( new FileInputStream(file)));
    } catch (@SuppressWarnings("unused") final Exception e) {
      return null;
    }
  }

  /**
   * Compute a signature for the file pointed to by the specified url.
   * @param url the url path to where the file is.
   * @return a hexadecimal representation of the signature.
   */
  public static String computeSignature(final URL url) {
    try {
      return computeSignature(url.openStream());
    } catch (@SuppressWarnings("unused") final Exception e) {
      return null;
    }
  }

  /**
   * Compute a signature for the data in the specified stream.
   * @param is the stream from which to compute the signature.
   * @return a hexadecimal representation of the signature.
   */
  public static String computeSignature(final InputStream is) {
    try {
      // compute the signature
      final MessageDigest digest = MessageDigest.getInstance(SIGNATURE_ALGORITHM);
      final byte[] buffer = new byte[2048];
      int numBytes;
      while ((numBytes = is.read(buffer)) != -1) digest.update(buffer, 0, numBytes);
      final byte[] sig = digest.digest();
      // convert the signature to a hexadecimal string
      return StringUtils.toHexString(sig);
    } catch (final Exception e) {
      e.printStackTrace();
    } finally {
      StreamUtils.closeSilent(is);
    }
    return null;
  }

  /**
   * Compute the actual file name for a library, based on the original name and its signature.
   * The resulting file name shoud be in the form <code>folder/<i>name</i>-<i>signature</i>.jar</code>
   * @param rootDir the root folder for the file location.
   * @param libName the original name of the library.
   * @param signature the file's signature.
   * @return a normalized fiie path.
   */
  public static File getLibFilePath(final String rootDir, final String libName, final String signature) {
    final StringBuilder sb = new StringBuilder();
    sb.append(rootDir).append('/');
    final int n = libName.lastIndexOf('.');
    final String ext = libName.substring(n);
    final String s = libName.substring(0, n);
    sb.append(s).append('-').append(signature).append(ext);
    return new File(sb.toString());
  }

  /**
   * Create a new ClassPath by scanning the specefied folder for jar files.
   * @param rootFolder the folder to scan.
   * @return a {@link ClassPath} object loaded with the files found in the folder.
   */
  public static ClassPath createClassPathFromRootFolder(final String rootFolder) {
    final ClassPathImpl cp = new ClassPathImpl(rootFolder);
    try {
      return cp.loadFromFileSystem();
    } catch (final Exception e) {
      e.printStackTrace();
    }
    return cp;
  }

  /**
   * Create a new ClassPath from a specified array of jar file paths.
   * @param files the paths of the files to add to the classpath.
   * @return a {@link ClassPath} object containig the files paths and their associated signatures.
   */
  public static ClassPath createClassPath(final String...files) {
    ClassPathImpl cp = null;
    if ((files != null) && (files.length > 0)) {
      cp = new ClassPathImpl();
      for (final String filename: files) {
        final String signature = computeSignature(filename);
        cp.addElement(filename, signature);
      }
    }
    return cp;
  }

  /**
   * Compute a {@link ClassPath} from a file pattern expression within a list of arguments.<br>
   * Example argument: <code>-c "*1.jar|*2.jar"</code> (note the quotes enclosing the pattern,
   * to avoid the OS shell interpreting the string).<br>
   * This pattern will match all the jar files ending with 1 or 2.
   * @param rootDir the directory in which the files are located.
   * @param args a list of arguments provided on the command line.
   * @return a new ClassPath object, or null if no matching file could be found or no classpath argument was specified.
   */
  public static ClassPath createClassPathFromArguments(final String rootDir, final String[] args) {
    if ((args == null) || (args.length <= 0)) return null;
    ClassPath cp = null;
    for (int i=0; i<args.length; i++) {
      if ("-c".equalsIgnoreCase(args[i])) {
        // convert the file pattern to a java regex
        final String regex = wildcardToRegex(args[i + 1]);
        final Pattern pattern = Pattern.compile(regex);

        // get the files in the root dir that match the pattern
        final File dir = new File(rootDir);
        final File[] files = dir.listFiles(new FileFilter() {
          @Override
          public boolean accept(final File pathname) {
            return pattern.matcher(pathname.getName()).matches();
          }
        });

        // build the classpath from the matching files
        if ((files != null) && (files.length > 0)) {
          cp = new ClassPathImpl();
          for (final File file: files) {
            final String name = file.getName();
            final String signature = computeSignature(file);
            if (signature != null) cp.addElement(name, signature);
          }
        }
        break;
      }
    }
    return cp;
  }

  /**
   * Compute a {@link RepositoryFilter} from a file pattern expression within a list of arguments.<br>
   * Example argument: <code>-d "*1.jar|*2.jar"</code> (note the quotes enclosing the pattern,
   * to avoid the OS shell interpreting the string).<br>
   * This pattern will match all the jar files ending with 1 or 2.
   * @param args a list of arguments provided on the command line.
   * @return a new ClassPath object, or null if no matching file could be found.
   */
  public static RepositoryFilter getFilterFromArguments(final String[] args) {
    if ((args == null) || (args.length <= 0)) return null;
    RepositoryFilter filter = null;
    for (int i=0; i<args.length; i++) {
      if ("-d".equalsIgnoreCase(args[i])) {
        final String regex = args[i + 1];
        filter = new RepositoryFilter.RegExFilter(regex);
        break;
      }
    }
    return filter;
  }

  /**
   * Convert a 'wildcard' pattern pattern into a java regex pattern.
   * @param patternString the wildcard pattern to convert.
   * @return a <code>java.util.regex.Pattern</code> instance.
   */
  public static String wildcardToRegex(final String patternString) {
    final StringBuffer sb = new StringBuffer();
    for (int i=0; i<patternString.length(); i++) {
      final char c = patternString.charAt(i);
      switch(c) {
        case '*':
        case '?':
          sb.append(".").append(c);
          break;
        case '.':
          sb.append("\\.");
          break;
        default:
          sb.append(Character.toLowerCase(c));
          break;
      }
    }
    return sb.toString();
  }
}