ClassLoaderUtilTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.commons.jxpath.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;

import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;

import junit.framework.TestCase;

/**
 * Tests org.apache.commons.jxpath.util.ClassLoaderUtil.
 */
public class ClassLoaderUtilTest extends TestCase {

  // These must be string literals, and not populated by calling getName() on
  // the respective classes, since the tests below will load this class in a
  // special class loader which may be unable to load those classes.
  private static final String TEST_CASE_CLASS_NAME = "org.apache.commons.jxpath.util.ClassLoaderUtilTest";
  private static final String EXAMPLE_CLASS_NAME = "org.apache.commons.jxpath.util.ClassLoadingExampleClass";

  private ClassLoader orginalContextClassLoader;

  /**
   * Setup for the tests.
   */
  @Override
public void setUp() {
    this.orginalContextClassLoader = Thread.currentThread().getContextClassLoader();
  }

  /**
   * Cleanup for the tests.
   */
  @Override
public void tearDown() {
    Thread.currentThread().setContextClassLoader(this.orginalContextClassLoader);
  }

  /**
   * Tests that JXPath cannot dynamically load a class, which is not visible to
   * its class loader, when the context class loader is null.
   */
  public void testClassLoadFailWithoutContextClassLoader() {
    Thread.currentThread().setContextClassLoader(null);
    final ClassLoader cl = new TestClassLoader(getClass().getClassLoader());
    executeTestMethodUnderClassLoader(cl, "callExampleMessageMethodAndAssertClassNotFoundJXPathException");
  }

  /**
   * Tests that JXPath can dynamically load a class, which is not visible to
   * its class loader, when the context class loader is set and can load the
   * class.
   */
  public void testClassLoadSuccessWithContextClassLoader() {
    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
    final ClassLoader cl = new TestClassLoader(getClass().getClassLoader());
    executeTestMethodUnderClassLoader(cl, "callExampleMessageMethodAndAssertSuccess");
  }

  /**
   * Tests that JXPath will use its class loader to dynamically load a
   * requested class when the context class loader is set but unable to load
   * the class.
   */
  public void testCurrentClassLoaderFallback() {
    final ClassLoader cl = new TestClassLoader(getClass().getClassLoader());
    Thread.currentThread().setContextClassLoader(cl);
    callExampleMessageMethodAndAssertSuccess();
  }

  /**
   * Tests that JXPath can dynamically load a class, which is visible to
   * its class loader, when there is no context class loader set.
   */
  public void testClassLoadSuccessWithoutContextClassLoader() {
    Thread.currentThread().setContextClassLoader(null);
    callExampleMessageMethodAndAssertSuccess();
  }

  /**
   * Performs a basic query that requires a class be loaded dynamically by
   * JXPath and asserts the dynamic class load fails.
   */
  public static void callExampleMessageMethodAndAssertClassNotFoundJXPathException() {
    final JXPathContext context = JXPathContext.newContext(new Object());
    try {
      context.selectSingleNode(EXAMPLE_CLASS_NAME+".getMessage()");
      fail("We should not be able to load "+EXAMPLE_CLASS_NAME+".");
    } catch ( final Exception e ) {
      assertTrue( e instanceof JXPathException );
    }
  }

  /**
   * Performs a basic query that requires a class be loaded dynamically by
   * JXPath and asserts the dynamic class load succeeds.
   */
  public static void callExampleMessageMethodAndAssertSuccess() {
    final JXPathContext context = JXPathContext.newContext(new Object());
    Object value;
    try {
      value = context.selectSingleNode(EXAMPLE_CLASS_NAME+".getMessage()");
      assertEquals("an example class", value);
    } catch ( final Exception e ) {
      fail(e.getMessage());
    }
  }

  /**
   * Loads this class through the given class loader and then invokes the
   * indicated no argument static method of the class.
   *
   * @param cl the class loader under which to invoke the method.
   * @param methodName the name of the static no argument method on this class
   * to invoke.
   */
  private void executeTestMethodUnderClassLoader(final ClassLoader cl, final String methodName) {
    Class testClass = null;
    try {
      testClass = cl.loadClass(TEST_CASE_CLASS_NAME);
    } catch (final ClassNotFoundException e) {
      fail(e.getMessage());
    }
    Method testMethod = null;
    try {
      testMethod = testClass.getMethod(methodName, null);
    } catch (final SecurityException | NoSuchMethodException e) {
      fail(e.getMessage());
    }

    try {
      testMethod.invoke(null, null);
    } catch (final IllegalArgumentException | IllegalAccessException e) {
      fail(e.getMessage());
    } catch (final InvocationTargetException e) {
      if (e.getCause() instanceof RuntimeException) {
        // Allow the runtime exception to propagate up.
        throw (RuntimeException) e.getCause();
      }
    }
  }

  /**
   * A simple class loader which delegates all class loading to its parent
   * with two exceptions. First, attempts to load the class
   * {@code org.apache.commons.jxpath.util.ClassLoaderUtilTest} will
   * always result in a ClassNotFoundException. Second, loading the class
   * {@code org.apache.commons.jxpath.util.ClassLoadingExampleClass} will
   * result in the class being loaded by this class loader, regardless of
   * whether the parent can/has loaded it.
   *
   */
  private static final class TestClassLoader extends ClassLoader {
    private Class testCaseClass = null;

    public TestClassLoader(final ClassLoader classLoader) {
      super(classLoader);
    }

    @Override
    public synchronized Class loadClass(final String name, final boolean resolved) throws ClassNotFoundException {
      if ( EXAMPLE_CLASS_NAME.equals(name) ) {
        throw new ClassNotFoundException();
      }
      else if ( TEST_CASE_CLASS_NAME.equals(name) ) {
        if ( testCaseClass == null ) {
          final URL clazzUrl = getParent().getResource("org/apache/commons/jxpath/util/ClassLoaderUtilTest.class");

          final ByteArrayOutputStream out = new ByteArrayOutputStream();
          InputStream in = null;
          try {
            in = clazzUrl.openStream();
            final byte[] buffer = new byte[2048];
            for( int read = in.read(buffer); read > -1; read = in.read(buffer) ) {
              out.write(buffer, 0, read);
            }
          } catch ( final IOException e ) {
            throw new ClassNotFoundException("Could not read class from resource "+clazzUrl+".", e);
          } finally {
            try { in.close(); } catch ( final Exception e ) { }
            try { out.close(); } catch ( final Exception e ) { }
          }

          final byte[] clazzBytes = out.toByteArray();
          this.testCaseClass = this.defineClass(TEST_CASE_CLASS_NAME, clazzBytes, 0, clazzBytes.length);
        }
        return this.testCaseClass;
      }
      return getParent().loadClass(name);
    }
  }
}