    001/*
    002    Licensed to the Apache Software Foundation (ASF) under one
    003    or more contributor license agreements.  See the NOTICE file
    004    distributed with this work for additional information
    005    regarding copyright ownership.  The ASF licenses this file
    006    to you under the Apache License, Version 2.0 (the
    007    "License"); you may not use this file except in compliance
    008    with the License.  You may obtain a copy of the License at
    009
    010       http://www.apache.org/licenses/LICENSE-2.0
    011
    012    Unless required by applicable law or agreed to in writing,
    013    software distributed under the License is distributed on an
    014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015    KIND, either express or implied.  See the License for the
    016    specific language governing permissions and limitations
    017    under the License.
    018 */
    019package org.apache.wiki.util;
    020
    021import org.apache.commons.io.FileUtils;
    022import org.apache.commons.lang3.StringUtils;
    023import org.apache.commons.lang3.Strings;
    024import org.apache.logging.log4j.LogManager;
    025import org.apache.logging.log4j.Logger;
    026import org.jdom2.Element;
    027
    028import java.io.File;
    029import java.io.IOException;
    030import java.lang.reflect.Constructor;
    031import java.net.JarURLConnection;
    032import java.net.MalformedURLException;
    033import java.net.URL;
    034import java.net.URLClassLoader;
    035import java.util.ArrayList;
    036import java.util.Enumeration;
    037import java.util.Iterator;
    038import java.util.List;
    039import java.util.Map;
    040import java.util.concurrent.ConcurrentHashMap;
    041import java.util.jar.JarEntry;
    042import java.util.jar.JarFile;
    043
    044/**
    045 * Contains useful utilities for class file manipulation. This is a static class, so there is no need to instantiate it.
    046 *
    047 * @since 2.1.29.
    048 */
    049public final class ClassUtil {
    050
    051    private static final Logger LOG = LogManager.getLogger(ClassUtil.class);
    052
    053    /** The location of the classmappings.xml document. It will be searched for in the classpath. Its value is "{@value}". */
    054    public  static final String MAPPINGS = "ini/classmappings.xml";
    055
    056    /** The location of the classmappings-extra.xml document. It will be searched for in the classpath. Its value is "{@value}". */
    057    public  static final String MAPPINGS_EXTRA = "ini/classmappings-extra.xml";
    058
    059    /** Initialize the class mappings document. */
    060    private static final Map< String, String > c_classMappings = populateClassMappingsFrom( MAPPINGS );
    061
    062    /** Initialize the class mappings extra document. */
    063    private static final Map< String, String > c_classMappingsExtra = populateClassMappingsFrom( MAPPINGS_EXTRA ) ;
    064
    065    private static boolean classLoaderSetup;
    066    private static ClassLoader loader;
    067
    068    private static Map< String, String > populateClassMappingsFrom( final String fileLoc ) {
    069        final Map< String, String > map = new ConcurrentHashMap<>();
    070        final List< Element > nodes = XmlUtil.parse( fileLoc, "/classmappings/mapping" );
    071
    072        if( !nodes.isEmpty() ) {
    073            for( final Element f : nodes ) {
    074                final String key = f.getChildText( "requestedClass" );
    075                final String className = f.getChildText( "mappedClass" );
    076
    077                map.put( key, className );
    078                LOG.debug( "Mapped class '{}' to class '{}'", key, className );
    079            }
    080        } else {
    081            LOG.info( "Didn't find class mapping document in {}", MAPPINGS );
    082        }
    083        return map;
    084    }
    085
    086    /**
    087     * Private constructor to prevent direct instantiation.
    088     */
    089    private ClassUtil() {}
    090
    091    /**
    092     *  Attempts to find a class from a collection of packages.  This will first
    093     *  attempt to find the class based on just the className parameter, but
    094     *  should that fail, will iterate through the "packages" -list, prefixes
    095     *  the package name to the className, and then tries to find the class
    096     *  again.
    097     *
    098     * @param packages A List of Strings, containing different package names.
    099     *  @param className The name of the class to find.
    100     * @return The class, if it was found.
    101     *  @throws ClassNotFoundException if this particular class cannot be found from the list.
    102     */
    103    @SuppressWarnings( "unchecked" )
    104    public static < T > Class< T > findClass( final List< String > packages,  final List< String > externaljars, final String className ) throws ClassNotFoundException {
    105        if (!classLoaderSetup) {
    106            loader = setupClassLoader(externaljars);
    107        }
    108
    109        try {
    110            return ( Class< T > )loader.loadClass( className );
    111        } catch( final ClassNotFoundException e ) {
    112            LOG.debug(e.getMessage(), e);
    113            for( final String packageName : packages ) {
    114                try {
    115                    return ( Class< T > )loader.loadClass( packageName + "." + className );
    116                } catch( final ClassNotFoundException ex ) {
    117                    // This is okay, we go to the next package.
    118                    LOG.debug(e.getMessage(), e);
    119                }
    120            }
    121
    122        }
    123
    124        throw new ClassNotFoundException( "Class '" + className + "' not found in search path!" );
    125    }
    126
    127    /**
    128     * Set up the plugin classloader, checking if there are external JARS to add.
    129     *
    130     * @param externaljars external jars to load into the classloader.
    131     * @return the classloader that can load classes from the configured external jars or, if not specified, the classloader that loaded this class.
    132     */
    133    private static ClassLoader setupClassLoader( final List< String > externaljars) {
    134        classLoaderSetup = true;
    135        LOG.info( "setting up classloaders for external (plugin) jars" );
    136        if(externaljars.isEmpty()) {
    137            LOG.info( "no external jars configured, using standard classloading" );
    138            return ClassUtil.class.getClassLoader();
    139        }
    140        final URL[] urls = new URL[externaljars.size()];
    141        int i = 0;
    142        for( final String externaljar : externaljars ) {
    143            try {
    144                final File jarFile = new File( externaljar );
    145                final URL ucl = jarFile.toURI().toURL();
    146                urls[ i++ ] = ucl;
    147                LOG.info( "added {} to list of external jars", ucl );
    148            } catch( final MalformedURLException e ) {
    149                LOG.error( "exception ({}) while setting up classloaders for external jar: {}, continuing without external jars.", e.getMessage(), externaljar );
    150            }
    151        }
    152
    153        if( i == 0 ) {
    154            LOG.error( "all external jars threw an exception while setting up classloaders for them, continuing with standard classloading. " +
    155                       "See https://jspwiki-wiki.apache.org/Wiki.jsp?page=InstallingPlugins for help on how to install custom plugins." );
    156            return ClassUtil.class.getClassLoader();
    157        }
    158
    159        return new URLClassLoader(urls, ClassUtil.class.getClassLoader());
    160    }
    161
    162    /**
    163     * It will first attempt to instantiate the class directly from the className, and will then try to prefix it with the packageName.
    164     *
    165     * @param packageName A package name (such as "org.apache.wiki.plugins").
    166     * @param className The class name to find.
    167     * @return The class, if it was found.
    168     * @throws ClassNotFoundException if this particular class cannot be found.
    169     */
    170    @SuppressWarnings( "unchecked" )
    171    public static < T > Class< T > findClass( final String packageName, final String className ) throws ClassNotFoundException {
    172        try {
    173            return ( Class< T > )ClassUtil.class.getClassLoader().loadClass( className );
    174        } catch( final ClassNotFoundException e ) {
    175            return ( Class< T > )ClassUtil.class.getClassLoader().loadClass( packageName + "." + className );
    176        }
    177    }
    178
    179    /**
    180     * Lists all the files in classpath under a given package.
    181     *
    182     * @param rootPackage the base package. Can be {code null}.
    183     * @return all files entries in classpath under the given package
    184     */
    185    public static List< String > classpathEntriesUnder( final String rootPackage ) {
    186        final List< String > results = new ArrayList<>();
    187        Enumeration< URL > en = null;
    188        if( StringUtils.isNotEmpty( rootPackage ) ) {
    189            try {
    190                en = ClassUtil.class.getClassLoader().getResources( rootPackage );
    191            } catch( final IOException e ) {
    192                LOG.error( e.getMessage(), e );
    193            }
    194        }
    195
    196        while( en != null && en.hasMoreElements() ) {
    197            final URL url = en.nextElement();
    198            try {
    199                if( "jar".equals( url.getProtocol() ) ) {
    200                    jarEntriesUnder( results, ( JarURLConnection )url.openConnection(), rootPackage );
    201                } else if( "file".equals( url.getProtocol() ) ) {
    202                    fileEntriesUnder( results, new File( url.getFile() ), rootPackage );
    203                }
    204
    205            } catch( final IOException ioe ) {
    206                LOG.error( ioe.getMessage(), ioe );
    207            }
    208        }
    209        return results;
    210    }
    211
    212    /**
    213     * Searchs for all the files in classpath under a given package, for a given {@link File}. If the
    214     * {@link File} is a directory all files inside it are stored, otherwise the {@link File} itself is
    215     * stored
    216     *
    217     * @param results collection in which the found entries are stored
    218     * @param file given {@link File} to search in.
    219     * @param rootPackage base package.
    220     */
    221    static void fileEntriesUnder( final List< String > results, final File file, final String rootPackage ) {
    222        LOG.debug( "scanning [{}]", file.getName() );
    223        if( file.isDirectory() ) {
    224            final Iterator< File > files = FileUtils.iterateFiles( file, null, true );
    225            while( files.hasNext() ) {
    226                final File subfile = files.next();
    227                // store an entry similar to the jarSearch(..) below ones
    228                final String entry = Strings.CS.replace( subfile.getAbsolutePath(), file.getAbsolutePath() + File.separatorChar, StringUtils.EMPTY );
    229                results.add( rootPackage + "/" + entry );
    230            }
    231        } else {
    232            results.add( file.getName() );
    233        }
    234    }
    235
    236    /**
    237     * Searchs for all the files in classpath under a given package, for a given {@link JarURLConnection}.
    238     *
    239     * @param results collection in which the found entries are stored
    240     * @param jurlcon given {@link JarURLConnection} to search in.
    241     * @param rootPackage base package.
    242     */
    243    static void jarEntriesUnder( final List< String > results, final JarURLConnection jurlcon, final String rootPackage ) {
    244        try( final JarFile jar = jurlcon.getJarFile() ) {
    245            LOG.debug( "scanning [{}]", jar.getName() );
    246            final Enumeration< JarEntry > entries = jar.entries();
    247            while( entries.hasMoreElements() ) {
    248                final JarEntry entry = entries.nextElement();
    249                if( entry.getName().startsWith( rootPackage ) && !entry.isDirectory() ) {
    250                    results.add( entry.getName() );
    251                }
    252            }
    253        } catch( final IOException ioe ) {
    254            LOG.error( ioe.getMessage(), ioe );
    255        }
    256    }
    257
    258    /**
    259     *  This method is used to locate and instantiate a mapped class.
    260     *  You may redefine anything in the resource file which is located in your classpath
    261     *  under the name <code>ClassUtil.MAPPINGS ({@value #MAPPINGS})</code>.
    262     *  <p>
    263     *  This is an extremely powerful system, which allows you to remap many of
    264     *  the JSPWiki core classes to your own class.  Please read the documentation
    265     *  included in the default <code>{@value #MAPPINGS}</code> file to see
    266     *  how this method works.
    267     *
    268     *  @param requestedClass The name of the class you wish to instantiate.
    269     *  @return An instantiated Object.
    270     *  @throws IllegalArgumentException If the class cannot be found or instantiated.
    271     *  @throws ReflectiveOperationException If the class cannot be found or instantiated.
    272     *  @since 2.5.40
    273     */
    274    public static < T > T getMappedObject( final String requestedClass ) throws ReflectiveOperationException, IllegalArgumentException {
    275        final Object[] initargs = {};
    276        return getMappedObject( requestedClass, initargs );
    277    }
    278
    279    /**
    280     *  This method is used to locate and instantiate a mapped class.
    281     *  You may redefine anything in the resource file which is located in your classpath
    282     *  under the name <code>{@value #MAPPINGS}</code>.
    283     *  <p>
    284     *  This is an extremely powerful system, which allows you to remap many of
    285     *  the JSPWiki core classes to your own class.  Please read the documentation
    286     *  included in the default <code>{@value #MAPPINGS}</code> file to see
    287     *  how this method works.
    288     *  <p>
    289     *  This method takes in an object array for the constructor arguments for classes
    290     *  which have more than two constructors.
    291     *
    292     *  @param requestedClass The name of the class you wish to instantiate.
    293     *  @param initargs The parameters to be passed to the constructor. May be <code>null</code>.
    294     *  @return An instantiated Object.
    295     *  @throws IllegalArgumentException If the class cannot be found or instantiated.
    296     *  @throws ReflectiveOperationException If the class cannot be found or instantiated.
    297     *  @since 2.5.40
    298     */
    299    @SuppressWarnings( "unchecked" )
    300    public static < T > T getMappedObject( final String requestedClass, final Object... initargs ) throws ReflectiveOperationException, IllegalArgumentException {
    301        final Class< ? > cl = getMappedClass( requestedClass );
    302        return ( T )buildInstance( cl, initargs );
    303    }
    304
    305    /**
    306     *  Finds a mapped class from the c_classMappings list.  If there is no mappped class, will use the requestedClass.
    307     *
    308     *  @param requestedClass requested class.
    309     *  @return A Class object which you can then instantiate.
    310     *  @throws ClassNotFoundException if the class is not found.
    311     */
    312    public static Class< ? > getMappedClass( final String requestedClass ) throws ClassNotFoundException {
    313        String mappedClass = c_classMappings.get( requestedClass );
    314        if( mappedClass == null ) {
    315            mappedClass = requestedClass;
    316        }
    317
    318        return Class.forName( mappedClass );
    319    }
    320
    321    /**
    322     * checks if {@code srcClassName} is a subclass of {@code parentClassname}.
    323     *
    324     * @param srcClassName expected subclass.
    325     * @param parentClassName expected parent class.
    326     * @return {@code true} if {@code srcClassName} is a subclass of {@code parentClassname}, {@code false} otherwise.
    327     */
    328    public static boolean assignable( final String srcClassName, final String parentClassName ) {
    329        try {
    330            final Class< ? > src = Class.forName( srcClassName );
    331            final Class< ? > parent = Class.forName( parentClassName );
    332            return parent.isAssignableFrom( src );
    333        } catch( final Exception e ) {
    334            LOG.error( e.getMessage(), e );
    335        }
    336        return false;
    337    }
    338
    339    /**
    340     * Checks if a given class exists in classpath.
    341     *
    342     * @param className the class to check for existence.
    343     * @return {@code true} if it exists, {@code false} otherwise.
    344     */
    345    public static boolean exists( final String className ) {
    346        try {
    347            Class.forName( className, false, ClassUtil.class.getClassLoader() );
    348            return true;
    349        } catch( final ClassNotFoundException e ) {
    350            LOG.debug(e.getMessage(), e);
    351            return false;
    352        }
    353    }
    354
    355    public static Map< String, String > getExtraClassMappings() {
    356        return c_classMappingsExtra;
    357    }
    358
    359    /**
    360     * This method is used to instantiate a given class.
    361     *
    362     * @param className The name of the class you wish to instantiate.
    363     * @return An instantiated Object.
    364     * @throws ReflectiveOperationException If the class cannot be found or instantiated.
    365     * @since 2.11.1
    366     */
    367    public static < T > T buildInstance( final String className ) throws ReflectiveOperationException {
    368        return buildInstance( "", className );
    369    }
    370
    371    /**
    372     * This method is used to instantiate a given class.
    373     * <p>
    374     * * It will first attempt to instantiate the class directly from the className, and will then try to prefix it with the packageName.
    375     *
    376     * @param packageName A package name (such as "org.apache.wiki.plugins").
    377     * @param className The class name to find.
    378     * @return An instantiated Object.
    379     * @throws ReflectiveOperationException If the class cannot be found or instantiated.
    380     * @since 2.11.1
    381     */
    382    public static < T > T buildInstance( final String packageName, final String className ) throws ReflectiveOperationException {
    383        return buildInstance( findClass( packageName, className ) );
    384    }
    385
    386    /**
    387     * This method is used to instantiate a given class.
    388     *
    389     * @param from The name of the class you wish to instantiate.
    390     * @return An instantiated Object.
    391     * @throws ReflectiveOperationException If the class cannot be found or instantiated.
    392     * @since 2.11.1
    393     */
    394    public static < T > T buildInstance( final Class< T > from ) throws ReflectiveOperationException {
    395        final Object[] initArgs = {};
    396        return buildInstance( from, initArgs );
    397    }
    398
    399    /**
    400     * This method is used to instantiate a given class.
    401     * <p>
    402     * This method takes in an object array for the constructor arguments for classes
    403     * which have more than two constructors.
    404     *
    405     * @param from The name of the class you wish to instantiate.
    406     * @param initArgs The parameters to be passed to the constructor. May be <code>null</code>.
    407     * @return An instantiated Object.
    408     * @throws ReflectiveOperationException If the class cannot be found or instantiated.
    409     * @since 2.11.1
    410     */
    411    @SuppressWarnings( "unchecked" )
    412    public static < T > T buildInstance( final Class< T > from, final Object... initArgs ) throws ReflectiveOperationException {
    413        final Constructor< ? >[] ctors = from.getConstructors();
    414
    415        //  Try to find the proper constructor by comparing the initargs array classes and the constructor types.
    416        for( final Constructor< ? > ctor : ctors ) {
    417            final Class< ? >[] params = ctor.getParameterTypes();
    418            if( params.length == initArgs.length ) {
    419                for( int arg = 0; arg < initArgs.length; arg++ ) {
    420                    if( params[ arg ].isAssignableFrom( initArgs[ arg ].getClass() ) ) {
    421                        //  Ha, found it!  Instantiating and returning...
    422                        return ( T )ctor.newInstance( initArgs );
    423                    }
    424                }
    425            }
    426        }
    427        //  No arguments, so we can just call a default constructor and ignore the arguments.
    428        return from.getDeclaredConstructor().newInstance();
    429    }
    430
    431}