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