/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.libraries.base.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.StringTokenizer; /** * A collection of useful static utility methods for handling classes and object instantiation. * * @author Thomas Morgner */ public final class ObjectUtilities { private static final Log LOGGER = LogFactory.getLog( ObjectUtilities.class ); /** * A constant for using the TheadContext as source for the classloader. */ public static final String THREAD_CONTEXT = "ThreadContext"; /** * A constant for using the ClassContext as source for the classloader. */ public static final String CLASS_CONTEXT = "ClassContext"; /** * By default use the thread context. */ private static String classLoaderSource = THREAD_CONTEXT; /** * The custom classloader to be used (if not null). */ private static ClassLoader classLoader; private static final Integer[] EMPTY_VERSIONS = new Integer[ 0 ]; /** * Default constructor - private. */ private ObjectUtilities() { } /** * Returns the internal configuration entry, whether the classloader of the thread context or the context classloader * should be used. * * @return the classloader source, either THREAD_CONTEXT or CLASS_CONTEXT. */ public static String getClassLoaderSource() { return classLoaderSource; } /** * Defines the internal configuration entry, whether the classloader of the thread context or the context classloader * should be used. * <p/> * This setting can only be defined using the API, there is no safe way to put this into an external configuration * file. * * @param classLoaderSource the classloader source, either THREAD_CONTEXT or CLASS_CONTEXT. */ public static void setClassLoaderSource( final String classLoaderSource ) { ObjectUtilities.classLoaderSource = classLoaderSource; } /** * Returns <code>true</code> if the two objects are equal OR both <code>null</code>. * * @param o1 object 1 (<code>null</code> permitted). * @param o2 object 2 (<code>null</code> permitted). * @return <code>true</code> or <code>false</code>. */ public static boolean equal( final Object o1, final Object o2 ) { if ( o1 == o2 ) { return true; } if ( o1 != null ) { return o1.equals( o2 ); } else { return false; } } /** * Performs a comparison on two file objects to determine if they refer to the same file. The * <code>File.equals()</code> method requires that the files refer to the same file in the same way (relative vs. * absolute). * * @param file1 the first file (<code>null</code> permitted). * @param file2 the second file (<code>null</code> permitted). * @return <code>true</code> if the files refer to the same file, <code>false</code> otherwise */ public static boolean equals( final File file1, final File file2 ) { if ( file1 == file2 ) { return true; } if ( file1 != null && file2 != null ) { try { return file1.getCanonicalFile().equals( file2.getCanonicalFile() ); } catch ( IOException ioe ) { // There was an error accessing the filesystem return file1.equals( file2 ); } } return false; } /** * Returns a clone of the specified object, if it can be cloned, otherwise throws a CloneNotSupportedException. * * @param object the object to clone (<code>null</code> not permitted). * @return A clone of the specified object. * @throws CloneNotSupportedException if the object cannot be cloned. */ public static Object clone( final Object object ) throws CloneNotSupportedException { if ( object == null ) { throw new IllegalArgumentException( "Null 'object' argument." ); } final Class aClass = object.getClass(); if ( aClass.isArray() ) { final int length = Array.getLength( object ); final Object clone = Array.newInstance( aClass.getComponentType(), length ); //noinspection SuspiciousSystemArraycopy System.arraycopy( object, 0, clone, 0, length ); return object; } try { final Method method = aClass.getMethod( "clone", (Class[]) null ); if ( Modifier.isPublic( method.getModifiers() ) ) { return method.invoke( object, (Object[]) null ); } throw new CloneNotSupportedException( "Failed to clone: Method 'clone()' is not public on class " + aClass ); } catch ( NoSuchMethodException e ) { LOGGER.warn( "Object without clone() method is impossible on class " + aClass, e ); } catch ( IllegalAccessException e ) { LOGGER.warn( "Object.clone(): unable to call method 'clone()' on class " + aClass, e ); } catch ( InvocationTargetException e ) { LOGGER.warn( "Object without clone() method is impossible on class " + aClass, e ); } throw new CloneNotSupportedException ( "Failed to clone: Clone caused an Exception while cloning type " + aClass ); } /** * Redefines the custom classloader. * * @param classLoader the new classloader or null to use the default. */ public static synchronized void setClassLoader( final ClassLoader classLoader ) { ObjectUtilities.classLoader = classLoader; } /** * Returns the custom classloader or null, if no custom classloader is defined. * * @return the custom classloader or null to use the default. */ public static ClassLoader getClassLoader() { return classLoader; } /** * Returns the classloader, which was responsible for loading the given class. * * @param c the classloader, either an application class loader or the boot loader. * @return the classloader, never null. * @throws SecurityException if the SecurityManager does not allow to grab the context classloader. */ public static ClassLoader getClassLoader( final Class c ) { final String localClassLoaderSource; synchronized( ObjectUtilities.class ) { if ( classLoader != null ) { return classLoader; } localClassLoaderSource = classLoaderSource; } if ( "ThreadContext".equals( localClassLoaderSource ) ) { final ClassLoader threadLoader = Thread.currentThread().getContextClassLoader(); if ( threadLoader != null ) { return threadLoader; } } // Context classloader - do not cache .. final ClassLoader applicationCL = c.getClassLoader(); if ( applicationCL == null ) { return ClassLoader.getSystemClassLoader(); } else { return applicationCL; } } /** * Returns the resource specified by the <strong>absolute</strong> name. * * @param name the name of the resource * @param c the source class * @return the url of the resource or null, if not found. */ public static URL getResource( final String name, final Class c ) { if ( name == null ) { throw new NullPointerException(); } final ClassLoader cl = getClassLoader( c ); if ( cl == null ) { return null; } return cl.getResource( name ); } /** * Returns the resource specified by the <strong>relative</strong> name. * * @param name the name of the resource relative to the given class * @param c the source class * @return the url of the resource or null, if not found. */ public static URL getResourceRelative( final String name, final Class c ) { if ( name == null ) { throw new NullPointerException(); } final ClassLoader cl = getClassLoader( c ); final String cname = convertName( name, c ); if ( cl == null ) { return null; } return cl.getResource( cname ); } /** * Transform the class-relative resource name into a global name by appending it to the classes package name. If the * name is already a global name (the name starts with a "/"), then the name is returned unchanged. * * @param name the resource name * @param c the class which the resource is relative to * @return the tranformed name. */ private static String convertName( final String name, Class c ) { if ( name.length() > 0 && name.charAt( 0 ) == '/' ) { // strip leading slash.. return name.substring( 1 ); } // we cant work on arrays, so remove them ... while ( c.isArray() ) { c = c.getComponentType(); } // extract the package ... final String baseName = c.getName(); final int index = baseName.lastIndexOf( '.' ); if ( index == -1 ) { return name; } final String pkgName = baseName.substring( 0, index ); return pkgName.replace( '.', '/' ) + '/' + name; } /** * Returns the inputstream for the resource specified by the <strong>absolute</strong> name. * * @param name the name of the resource * @param context the source class * @return the url of the resource or null, if not found. */ public static InputStream getResourceAsStream( final String name, final Class context ) { final URL url = getResource( name, context ); if ( url == null ) { return null; } try { return url.openStream(); } catch ( IOException e ) { return null; } } /** * Returns the inputstream for the resource specified by the <strong>relative</strong> name. * * @param name the name of the resource relative to the given class * @param context the source class * @return the url of the resource or null, if not found. */ public static InputStream getResourceRelativeAsStream ( final String name, final Class context ) { final URL url = getResourceRelative( name, context ); if ( url == null ) { return null; } try { return url.openStream(); } catch ( IOException e ) { return null; } } /** * Tries to create a new instance of the given class. This is a short cut for the common bean instantiation code. * * @param className the class name as String, never null. * @param source the source class, from where to get the classloader. * @return the instantiated object or null, if an error occured. * @deprecated This class is not typesafe and instantiates the specified object without any additional checks. */ public static Object loadAndInstantiate( final String className, final Class source ) { return loadAndInstantiate( className, source, null ); } /** * Tries to create a new instance of the given class. This is a short cut for the common bean instantiation code. This * method is a type-safe method and will not instantiate the class unless it is an instance of the given type. * * @param className the class name as String, never null. * @param source the source class, from where to get the classloader. * @param type the expected type of the object that is being instantiated. * @return the instantiated object, which is guaranteed to be of the given type, or null, if an error occured. */ public static <T> T loadAndInstantiate( final String className, final Class source, final Class<T> type ) { if ( className == null || className.length() == 0 ) { return null; } try { final ClassLoader loader = getClassLoader( source ); final Class c = Class.forName( className, false, loader ); return instantiateSafe( c, type ); } catch ( ClassNotFoundException e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( "Specified class " + className + " does not exist.", e ); } // sometimes, this one is expected. } catch ( NoClassDefFoundError e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( noClassDefFoundErrorMessage( className ), e ); } } return null; } public static <T> T instantiateSafe( final Class clazz, final Class<T> type ) { try { if ( type != null && type.isAssignableFrom( clazz ) == false ) { // this is unacceptable and means someone messed up the configuration LOGGER.warn( "Specified class " + clazz.getName() + " is not of expected type " + type ); return null; } //noinspection unchecked return (T) clazz.newInstance(); } catch ( NoClassDefFoundError e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( noClassDefFoundErrorMessage( clazz.getName() ), e ); } } catch ( Throwable e ) { // this is more severe than a class not being found at all if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( "Specified class " + clazz.getName() + " failed to instantiate correctly.", e ); } else { LOGGER.info( "Specified class " + clazz.getName() + " failed to instantiate correctly." ); } } return null; } private static String noClassDefFoundErrorMessage( String clazz ) { return "Specified class " + clazz + " cannot be loaded [NOCLASSDEFERROR]."; } public static <T> Class<? extends T> loadAndValidate( final String className, final Class source, final Class<T> type ) { if ( className == null || className.length() == 0 ) { return null; } try { final ClassLoader loader = getClassLoader( source ); final Class c = Class.forName( className, false, loader ); if ( type != null && type.isAssignableFrom( c ) == false ) { // this is unacceptable and means someone messed up the configuration LOGGER.warn( "Specified class " + className + " is not of expected type " + type ); return null; } //noinspection unchecked return (Class<? extends T>) c; } catch ( ClassNotFoundException e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( "Specified class " + className + " does not exist.", e ); } // sometimes, this one is expected. } catch ( NoClassDefFoundError e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( noClassDefFoundErrorMessage( className ), e ); } } catch ( Throwable e ) { // this is more severe than a class not being found at all if ( LOGGER.isDebugEnabled() ) { LOGGER.info( "Specified class " + className + " failed to instantiate correctly.", e ); } else { LOGGER.info( "Specified class " + className + " failed to instantiate correctly." ); } } return null; } /** * Checks whether the current JDK is at least JDK 1.4. * * @return true, if the JDK has been recognized as JDK 1.4, false otherwise. * @noinspection AccessOfSystemProperties */ public static boolean isJDK14() { try { final ClassLoader loader = getClassLoader( ObjectUtilities.class ); if ( loader != null ) { try { Class.forName( "java.util.RandomAccess", false, loader ); return true; } catch ( ClassNotFoundException e ) { return false; } } } catch ( Exception e ) { // the safe test failed, now lets check the system properties ... } // OK, the quick and dirty, but secure way failed. Lets try it // using the standard way. try { final String version = System.getProperty ( "java.vm.specification.version" ); // parse the beast... if ( version == null ) { return false; } final Integer[] versions = parseVersions( version ); final Integer[] target = new Integer[] { new Integer( 1 ), new Integer( 4 ) }; return ( ObjectUtilities.compareVersionArrays( versions, target ) >= 0 ); } catch ( Exception e ) { // if that fails too, we assume the safe "no-JDK 1.4" mode. return false; } } /** * Compares version numbers. * * @param a1 the first array. * @param a2 the second array. * @return -1 if a1 is less than a2, 0 if a1 and a2 are equal, and +1 otherwise. */ public static int compareVersionArrays( final Integer[] a1, final Integer[] a2 ) { final int length = Math.min( a1.length, a2.length ); for ( int i = 0; i < length; i++ ) { final Integer o1 = a1[ i ]; final Integer o2 = a2[ i ]; if ( o1 == null && o2 == null ) { // cannot decide .. continue; } if ( o1 == null ) { return 1; } if ( o2 == null ) { return -1; } final int retval = o1.compareTo( o2 ); if ( retval != 0 ) { return retval; } } return 0; } /** * Parses a version string into numbers. * * @param version the version. * @return the parsed version array. */ public static Integer[] parseVersions( final String version ) { if ( version == null ) { return EMPTY_VERSIONS; } final ArrayList<Integer> versions = new ArrayList<Integer>(); final StringTokenizer strtok = new StringTokenizer( version, "." ); while ( strtok.hasMoreTokens() ) { try { versions.add( new Integer( strtok.nextToken() ) ); } catch ( NumberFormatException nfe ) { break; } } return versions.toArray( new Integer[ versions.size() ] ); } /** * Compares two arrays and determines if they are equal. This method allows to pass <code>null</code> null references * for any of the arrays. * * @param array1 the first array to compare. * @param array2 the second array to compare. * @return true, if both arrays are equal or both arrays are null, false otherwise. */ public static boolean equalArray( final Object[] array1, final Object[] array2 ) { //noinspection ArrayEquality if ( array1 == array2 ) { return true; } else if ( array1 == null || array2 == null ) { return false; } else { return Arrays.equals( array1, array2 ); } } public static int hashCode( final Object[] array1 ) { if ( array1 == null ) { return 0; } return Arrays.hashCode( array1 ); } }