All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gemstone.gemfire.internal.JarClassLoader Maven / Gradle / Ivy

There is a newer version: 2.0-BETA
Show newest version
/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * 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. See accompanying
 * LICENSE file.
 */
package com.gemstone.gemfire.internal;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import com.gemstone.gemfire.LogWriter;
import com.gemstone.gemfire.cache.CacheClosedException;
import com.gemstone.gemfire.cache.CacheFactory;
import com.gemstone.gemfire.cache.Declarable;
import com.gemstone.gemfire.cache.execute.Function;
import com.gemstone.gemfire.cache.execute.FunctionService;
import com.gemstone.gemfire.internal.cache.GemFireCacheImpl;
import com.gemstone.gemfire.pdx.internal.TypeRegistry;

/**
 * ClassLoader for a single JAR file.
 * 
 * @author David Hoots
 * @since 7.0
 */
public class JarClassLoader extends ClassLoader {
  private final static MessageDigest messageDigest;

  private final String jarName;
  private final File file;
  private final byte[] md5hash;
  private final FileLock fileLock;
  private final Collection registeredFunctions = new ArrayList();

  private final ThreadLocal alreadyScanned = new ThreadLocal();

  // Lock used by ChannelInputStream (inner class) to prevent multiple threads from
  // trying to use the channel simultaneously.
  static final ReentrantLock channelLock = new ReentrantLock();

  private LogWriter logger;
  private byte[] jarByteContent;

  static {
    MessageDigest md = null;
    try {
      md = MessageDigest.getInstance("MD5");
    } catch (NoSuchAlgorithmException nsaex) {
      // Failure just means we can't do a simple compare for content equality
    }
    messageDigest = md;
  }

  public JarClassLoader(final File file, final String jarName, byte[] jarBytes) throws IOException {
    Assert.assertTrue(file != null, "file cannot be null");
    Assert.assertTrue(jarName != null, "jarName cannot be null");
    FileLock tempLock = null;
    try {
      @SuppressWarnings("resource")
      FileInputStream fileInputStream = new FileInputStream(file);
      tempLock = fileInputStream.getChannel().lock(0, file.length(), true);

      try {
        this.logger = CacheFactory.getAnyInstance().getLogger();
      } catch (CacheClosedException ccex) {
        // That's okay, logging will just go to stdout/stderr.
      }

      trace("Acquired shared file lock w/ channel: " + tempLock.channel() + ", for JAR: " + file.getAbsolutePath());

      if (file.length() == 0) {
        throw new FileNotFoundException("JAR file was truncated prior to obtaining a lock: " + jarName);
      }

      final byte[] fileContent = getJarContent();
      if (!Arrays.equals(fileContent, jarBytes)) {
        throw new FileNotFoundException("JAR file: " + file.getAbsolutePath() + ", was modified prior to obtaining a lock: "
            + jarName);
      }
        
      if (!isValidJarContent(jarBytes)) {
        if (tempLock != null) {
          tempLock.release();
          tempLock.channel().close();
          trace("Prematurely releasing shared file lock due to bad content for JAR file: " + file.getAbsolutePath()
              + ", w/ channel: " + tempLock.channel());
        }
        throw new IllegalArgumentException("File does not contain valid JAR content: " + file.getAbsolutePath());
      }
      
      Assert.assertTrue(jarBytes != null, "jarBytes cannot be null");

      // Temporarily save the contents of the JAR file until they can be processed by the
      // loadClassesandRegisterFunctions() method.
      this.jarByteContent = jarBytes;

      if (messageDigest != null) {
        this.md5hash = messageDigest.digest(this.jarByteContent);
      } else {
        this.md5hash = null;
      }

      this.file = file;
      this.jarName = jarName;
      this.fileLock = tempLock;

    } catch (FileNotFoundException fnfex) {
      if (tempLock != null) {
        tempLock.release();
        tempLock.channel().close();
        trace("Prematurely releasing shared file lock due to file not found for JAR file: " + file.getAbsolutePath()
            + ", w/ channel: " + tempLock.channel());
      }
      throw fnfex;
    }
  }

  /**
   * Peek into the JAR data and make sure that it is valid JAR content.
   * 
   * @param inputStream
   *          InputStream containing data to be validated.
   * 
   * @return True if the data has JAR content, false otherwise
   */
  private static boolean hasValidJarContent(final InputStream inputStream) {
    JarInputStream jarInputStream = null;
    boolean valid = false;

    try {
      jarInputStream = new JarInputStream(inputStream);
      valid = (jarInputStream.getNextJarEntry() != null);
    } catch (IOException ignore) {
      // Ignore this exception and just return false
    } finally {
      try {
        jarInputStream.close();
      } catch (IOException ioex) {
        // Ignore this exception and just return result
      }
    }

    return valid;
  }
  
  /**
   * Peek into the JAR data and make sure that it is valid JAR content.
   * 
   * @param jarBytes
   *          Bytes of data to be validated.
   * 
   * @return True if the data has JAR content, false otherwise
   */
  public static boolean isValidJarContent(final byte[] jarBytes) {
    return hasValidJarContent(new ByteArrayInputStream(jarBytes));
  }
  
  /**
   * Peek into the JAR data and make sure that it is valid JAR content.
   * 
   * @param jarFile
   *          File whose contents should be validated.
   * 
   * @return True if the data has JAR content, false otherwise
   */
  public static boolean hasValidJarContent(final File jarFile) {
    try {
    return hasValidJarContent(new FileInputStream(jarFile));
    } catch (IOException ioex) {
      return false;
    }
  }
  
  /**
   * Scan the JAR file and attempt to load all classes and register any function classes found.
   */
  // This method will process the contents of the JAR file as stored in this.jarByteContent
  // instead of reading from the original JAR file. This is done because we can't open up
  // the original file and then close it without releasing the shared lock that was obtained
  // in the constructor. Once this method is finished, all classes will have been loaded and
  // there will no longer be a need to hang on to the original contents so they will be
  // discarded.
  public synchronized void loadClassesAndRegisterFunctions() throws ClassNotFoundException {
    trace("Registering functions with JarClassLoader: " + this);

    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.jarByteContent);

    JarInputStream jarInputStream = null;
    try {
      jarInputStream = new JarInputStream(byteArrayInputStream);
      JarEntry jarEntry = jarInputStream.getNextJarEntry();

      while (jarEntry != null) {
        if (jarEntry.getName().endsWith(".class")) {
          trace("Attempting to load class: " + jarEntry.getName() + ", from JAR file: " + this.file.getAbsolutePath());

          final String className = jarEntry.getName().replaceAll("/", "\\.").substring(0, (jarEntry.getName().length() - 6));
          try {
            Class clazz = loadClass(className, true, false);
            Collection registerableFunctions = getRegisterableFunctionsFromClass(clazz);
            for (Function function : registerableFunctions) {
              FunctionService.registerFunction(function);
              trace("Registering function class: " + className + ", from JAR file: " + this.file.getAbsolutePath());
              this.registeredFunctions.add(function);
            }
          } catch (ClassNotFoundException cnfex) {
            error("Unable to load all classes from JAR file: " + this.file.getAbsolutePath(), cnfex);
            throw cnfex;
          } catch (NoClassDefFoundError ncdfex) {
            error("Unable to load all classes from JAR file: " + this.file.getAbsolutePath(), ncdfex);
            throw ncdfex;
          }
        }
        jarEntry = jarInputStream.getNextJarEntry();
      }
    } catch (IOException ioex) {
      error("Exception when trying to read class from ByteArrayInputStream", ioex);
    } finally {
      if (jarInputStream != null) {
        try {
          jarInputStream.close();
        } catch (IOException ioex) {
          error("Exception attempting to close JAR input stream", ioex);
        }
      }
    }
    this.jarByteContent = new byte[0];
  }

  synchronized void cleanUp() {
    for (Function function : this.registeredFunctions) {
      FunctionService.unregisterFunction(function.getId());
    }
    this.registeredFunctions.clear();

    try {
      TypeRegistry typeRegistry = ((GemFireCacheImpl) CacheFactory.getAnyInstance()).getPdxRegistry();
      if (typeRegistry != null) {
        typeRegistry.flushCache();
      }
    } catch (CacheClosedException ccex) {
      // That's okay, it just means there was nothing to flush to begin with
    }

    try {
      this.fileLock.release();
      this.fileLock.channel().close();
      trace("Released shared file lock on JAR file: " + this.file.getAbsolutePath() + ", w/ channel: " + this.fileLock.channel());
    } catch (IOException ioex) {
      error("Could not release the shared lock for JAR file: " + this.file.getAbsolutePath(), ioex);
    }
  }

  /**
   * Uses MD5 hashes to determine if the original byte content of this JarClassLoader is the same as that past in.
   * 
   * @param compareToBytes
   *          Bytes to compare the original content to
   * @return True of the MD5 hash is the same o
   */
  public boolean hasSameContent(final byte[] compareToBytes) {
    // If the MD5 hash can't be calculated then silently return no match
    if (messageDigest == null || this.md5hash == null) {
      return false;
    }

    byte[] compareToMd5 = messageDigest.digest(compareToBytes);
    trace("For JAR file: " + this.file.getAbsolutePath() + ", Comparing MD5 hash " + new String(this.md5hash) + " to "
        + new String(compareToMd5));
    return Arrays.equals(this.md5hash, compareToMd5);
  }

  private boolean alreadyScanned() {
    if (this.alreadyScanned.get() == null) {
      this.alreadyScanned.set(Boolean.FALSE);
    }
    return this.alreadyScanned.get();
  }

  @Override
  protected URL findResource(String resourceName) {
    URL returnURL = null;
    JarInputStream jarInputStream = null;
    
    try {
      ChannelInputStream channelInputStream = new ChannelInputStream(this.fileLock.channel());
      jarInputStream = new JarInputStream(channelInputStream);

      JarEntry jarEntry = jarInputStream.getNextJarEntry();
      while (jarEntry != null && !jarEntry.getName().equals(resourceName)) {
        jarEntry = jarInputStream.getNextJarEntry();
      }
      if (jarEntry != null) {
        try {
          returnURL = new URL("jar", "", this.file.toURI().toURL() + "!/" + jarEntry.getName());
        } catch (MalformedURLException muex) {
          error("Could not create resource URL from file URL", muex);
        }
      }
    } catch (IOException ioex) {
      error("Exception when trying to read class from ByteArrayInputStream", ioex);
    } finally {
      if (jarInputStream != null) {
        try {
          jarInputStream.close();
        } catch (IOException ioex) {
          error("Unable to close JAR input stream when finding resource", ioex);
        }
      }
    }

    return returnURL;
  }

  @Override
  protected Enumeration findResources(final String resourceName) {
    return new Enumeration() {
      private URL element = findResource(resourceName);

      @Override
      public boolean hasMoreElements() {
        return this.element != null;
      }

      @Override
      public URL nextElement() {
        if (this.element != null) {
          URL element = this.element;
          this.element = null;
          return element;
        }
        throw new NoSuchElementException();
      }
    };
  }

  @Override
  public Class loadClass(final String className) throws ClassNotFoundException {
    return (loadClass(className, true));
  }

  @Override
  public Class loadClass(final String className, final boolean resolveIt) throws ClassNotFoundException {
    return loadClass(className, resolveIt, true);
  }

  Class loadClass(final String className, final boolean resolveIt, final boolean useClassPathLoader)
      throws ClassNotFoundException {
    Class clazz = findLoadedClass(className);
    if (clazz != null) {
      return clazz;
    }

    try {
      clazz = findClass(className);
      if (resolveIt) {
        resolveClass(clazz);
      }
    } catch (ClassNotFoundException cnfex) {
      if (!useClassPathLoader) {
        throw cnfex;
      }
    }

    if (clazz == null) {
      try {
        this.alreadyScanned.set(true);
        return forName(className, ClassPathLoader.getLatest().getClassLoaders());
      } finally {
        this.alreadyScanned.set(false);
      }
    }

    return clazz;
  }

  // When loadClassesAndRegisterFunctions() is called and it starts to load classes, this method
  // may be called multiple times to resolve dependencies on other classes in the same or
  // another JAR file. During this stage, this.jarByteContent will contain the complete contents of
  // the JAR file and will be used when attempting to resolve these dependencies. Once
  // loadClassesAndRegisterFunctions() is complete it discards the data in this.jarByteContent.
  // However, at that point all of the classes available in the JAR file will already have been
  // loaded. Future calls to loadClass(...) will return the cached Class object for any
  // classes available in this JAR and findClass(...) will no longer be needed to find them.
  @Override
  protected Class findClass(String className) throws ClassNotFoundException {
    String formattedClassName = className.replaceAll("\\.", "/") + ".class";

    JarInputStream jarInputStream = null;
    if (this.jarByteContent.length == 0) {
      throw new ClassNotFoundException(className);
    }

    try {
      jarInputStream = new JarInputStream(new ByteArrayInputStream(this.jarByteContent));
      JarEntry jarEntry = jarInputStream.getNextJarEntry();

      while (jarEntry != null && !jarEntry.getName().equals(formattedClassName)) {
        jarEntry = jarInputStream.getNextJarEntry();
      }

      if (jarEntry != null) {
        byte[] buffer = new byte[1024];
        ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(buffer.length);

        int bytesRead = -1;
        while ((bytesRead = jarInputStream.read(buffer)) != -1) {
          byteOutStream.write(buffer, 0, bytesRead);
        }

        // Add the package first if it doesn't already exist
        int lastDotIndex = className.lastIndexOf('.');
        if (lastDotIndex != -1) {
          String pkgName = className.substring(0, lastDotIndex);
          Package pkg = getPackage(pkgName);
          if (pkg == null) {
            definePackage(pkgName, null, null, null, null, null, null, null);
          }
        }

        byte[] classBytes = byteOutStream.toByteArray();

        synchronized (this.file) {
          Class clazz = findLoadedClass(className);
          if (clazz == null) {
            clazz = defineClass(className, classBytes, 0, classBytes.length, null);
          }
          return clazz;
        }
      }
    } catch (IOException ioex) {
      error("Exception when trying to read class from ByteArrayInputStream", ioex);
    } finally {
      if (jarInputStream != null) {
        try {
          jarInputStream.close();
        } catch (IOException ioex) {
          error("Exception attempting to close JAR input stream", ioex);
        }
      }
    }

    throw new ClassNotFoundException(className);
  }

  /**
   * Check to see if the class implements the Function interface. If so, it will be registered with FunctionService.
   * Also, if the functions's class was originally declared in a cache.xml file then any properties specified at that
   * time will be reused when re-registering the function.
   * 
   * @param clazz
   *          Class to check for implementation of the Function class
   * @return A collection of Objects that implement the Function interface.
   */
  private Collection getRegisterableFunctionsFromClass(Class clazz) {
    final List registerableFunctions = new ArrayList();

    try {
      if (Function.class.isAssignableFrom(clazz) && !Modifier.isAbstract(clazz.getModifiers())) {
        boolean registerUninitializedFunction = true;
        if (Declarable.class.isAssignableFrom(clazz)) {
          try {
            final List propertiesList = ((GemFireCacheImpl) CacheFactory.getAnyInstance())
                .getDeclarableProperties(clazz.getName());

            if (propertiesList != null && propertiesList.size() != 0) {
              registerUninitializedFunction = false;
              // It's possible that the same function was declared multiple times in cache.xml
              // with different properties. So, register the function using each set of
              // properties.
              for (Properties properties : propertiesList) {
                @SuppressWarnings("unchecked")
                Function function = newFunction((Class) clazz, true);
                if (function != null) {
                  ((Declarable) function).init(properties);
                  if (function.getId() != null) {
                    registerableFunctions.add(function);
                  }
                }
              }
            }
          } catch (CacheClosedException ccex) {
            // That's okay, it just means there were no properties to init the function with
          }
        }

        if (registerUninitializedFunction) {
          @SuppressWarnings("unchecked")
          Function function = newFunction((Class) clazz, false);
          if (function != null && function.getId() != null) {
            registerableFunctions.add(function);
          }
        }
      }
    } catch (Exception ex) {
      error("Attempting to register function from JAR file: " + this.file.getAbsolutePath(), ex);
    }

    return registerableFunctions;
  }

  private Class forName(final String className, final Collection classLoaders) throws ClassNotFoundException {
    Class clazz = null;

    for (ClassLoader classLoader : classLoaders) {
      try {
        if (classLoader instanceof JarClassLoader) {
          if (!((JarClassLoader) classLoader).alreadyScanned()) {
            clazz = ((JarClassLoader) classLoader).loadClass(className, true, false);
          }
        } else {
          clazz = Class.forName(className, true, classLoader);
        }
        if (clazz != null) {
          return clazz;
        }
      } catch (SecurityException sex) {
        // Continue to next ClassLoader
      } catch (ClassNotFoundException cnfex) {
        // Continue to next ClassLoader
      }
    }
    throw new ClassNotFoundException(className);
  }

  private Function newFunction(final Class clazz, final boolean errorOnNoSuchMethod) {
    try {
      final Constructor constructor = clazz.getConstructor();
      return constructor.newInstance();
    } catch (NoSuchMethodException nsmex) {
      if (errorOnNoSuchMethod) {
        error("Zero-arg constructor is required, but not found for class: " + clazz.getName(), nsmex);
      } else {
        trace("Not registering function because it doesn't have a zero-arg constructor: " + clazz.getName());
      }
    } catch (SecurityException sex) {
      error("Zero-arg constructor of function not accessible for class: " + clazz.getName(), sex);
    } catch (IllegalAccessException iae) {
      error("Zero-arg constructor of function not accessible for class: " + clazz.getName(), iae);
    } catch (InvocationTargetException ite) {
      error("Error when attempting constructor for function for class: " + clazz.getName(), ite);
    } catch (InstantiationException ie) {
      error("Unable to instantiate function for class: " + clazz.getName(), ie);
    } catch (ExceptionInInitializerError eiiex) {
      error("Error during function initialization for class: " + clazz.getName(), eiiex);
    }
    return null;
  }

  private byte[] getJarContent() throws IOException {
    ChannelInputStream channelInputStream = new ChannelInputStream(this.fileLock.channel());
    final ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
    final byte[] bytes = new byte[4096];

    int bytesRead;
    while (((bytesRead = channelInputStream.read(bytes)) != -1)) {
      byteOutStream.write(bytes, 0, bytesRead);
    }
    channelInputStream.close();
    return byteOutStream.toByteArray();
  }

  private void error(final String message, final Throwable throwable) {
    if (this.logger == null) {
      System.err.println(message);
      throwable.printStackTrace(System.err);
    } else {
      this.logger.error(message, throwable);
    }
  }

  private void trace(final String message) {
    if (this.logger == null) {
      System.out.println(message);
    } else {
      this.logger.fine(message);
    }
  }

  public String getJarName() {
    return this.jarName;
  }

  public String getFileName() {
    return this.file.getName();
  }

  public String getFileCanonicalPath() throws IOException {
    return this.file.getCanonicalPath();
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((this.jarName == null) ? 0 : this.jarName.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    JarClassLoader other = (JarClassLoader) obj;
    if (this.jarName == null) {
      if (other.jarName != null)
        return false;
    } else if (!this.jarName.equals(other.jarName))
      return false;
    return true;
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder(getClass().getName());
    sb.append("@").append(System.identityHashCode(this)).append("{");
    sb.append("jarName=").append(this.jarName);
    sb.append(",file=").append(this.file.getAbsolutePath());
    sb.append(",md5hash=").append(Arrays.toString(this.md5hash));
    sb.append(",fileLock=").append(this.fileLock);
    sb.append("}");
    return sb.toString();
  }

  /**
   * When a lock is acquired it is done so through an open file (FileInputStream, etc.). If for any reason that same
   * file is used to open another input stream, when the second input stream is closed the file lock will not be held
   * (although an OverlappingFileLock exception will be thrown if an attempt is made to acquire the lock again). To get
   * around this problem, this class is used to wrap the original file channel used by the lock with an InputStream.
   * When this class is instantiated a lock is obtained to prevent other threads from attempting to use the file channel
   * at the same time. The file channel can then be read as with any other InputStream. When the input stream is closed,
   * instead of closing the file channel, the lock is released instead.
   * 
   * This class is thread safe. However, multiple instances cannot be created by the same thread. The reason for this is
   * that the lock will be obtained in all cases (it's reentrant), and then the channel position will be modified by
   * both instances causing arbitrary values being returned from the read() method.
   * 
   * @author David Hoots
   * @since 7.0
   */
  private class ChannelInputStream extends InputStream {
    private final FileChannel fileChannel;
    private final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1);

    ChannelInputStream(final FileChannel fileChannel) throws IOException {
      channelLock.lock();
      this.fileChannel = fileChannel;
      this.fileChannel.position(0);
    }

    @Override
    public int read() throws IOException {
      this.byteBuffer.rewind();
      if (this.fileChannel.read(this.byteBuffer) <= 0) {
        return -1;
      }
      this.byteBuffer.rewind();
      return (this.byteBuffer.get() & 255);
    }

    @Override
    public void close() {
      channelLock.unlock();
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy