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.log4j.Logger;
024import org.jdom2.Element;
025
026import java.io.File;
027import java.io.IOException;
028import java.lang.reflect.Constructor;
029import java.net.JarURLConnection;
030import java.net.MalformedURLException;
031import java.net.URL;
032import java.net.URLClassLoader;
033import java.util.ArrayList;
034import java.util.Enumeration;
035import java.util.Iterator;
036import java.util.List;
037import java.util.Map;
038import java.util.concurrent.ConcurrentHashMap;
039import java.util.jar.JarEntry;
040import java.util.jar.JarFile;
041
042/**
043 *  Contains useful utilities for class file manipulation.  This is a static class,
044 *  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 = Logger.getLogger(ClassUtil.class);
051
052    /** The location of the classmappings.xml document. It will be searched for in the classpath.  It's 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.  It's value is "{@value}". */
056    public  static final String MAPPINGS_EXTRA = "ini/classmappings-extra.xml";
057
058    /** Initialize the class mappings document. */
059    private static Map< String, String > c_classMappings = populateClassMappingsFrom( MAPPINGS );
060
061    /** Initialize the class mappings extra document. */
062    private static Map< String, String > c_classMappingsExtra = populateClassMappingsFrom( MAPPINGS_EXTRA ) ;
063
064    private static boolean classLoaderSetup = false;
065    private static ClassLoader loader = null;
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 '" + key + "' to class '" + 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    public static Class<?> findClass( final List< String > packages,  final List< String > externaljars, final String className ) throws ClassNotFoundException {
103        if (!classLoaderSetup) {
104            loader = setupClassLoader(externaljars);
105        }
106
107        try {
108            return loader.loadClass( className );
109        } catch( final ClassNotFoundException e ) {
110            for( final String packageName : packages ) {
111                try {
112                    return loader.loadClass( packageName + "." + className );
113                } catch( final ClassNotFoundException ex ) {
114                    // This is okay, we go to the next package.
115                }
116            }
117
118        }
119
120        throw new ClassNotFoundException( "Class '" + className + "' not found in search path!" );
121    }
122
123    /**
124     * Setup the plugin classloader, checking if there are external JARS to add.
125     * 
126     * @param externaljars external jars to load into the classloader.
127     * @return the classloader that can load classes from the configured external jars or, if not specified, the classloader that loaded this class.
128     */
129    private static ClassLoader setupClassLoader( final List< String > externaljars) {
130        classLoaderSetup = true;
131        log.info( "setting up classloaders for external (plugin) jars" );
132        if( externaljars.size() == 0 ) {
133            log.info( "no external jars configured, using standard classloading" );
134            return ClassUtil.class.getClassLoader();
135        }
136        final URL[] urls = new URL[externaljars.size()];
137        int i = 0;
138        for( final String externaljar : externaljars ) {
139            try {
140                final File jarFile = new File( externaljar );
141                final URL ucl = jarFile.toURI().toURL();
142                urls[ i++ ] = ucl;
143                log.info( "added " + ucl + " to list of external jars" );
144            } catch( final MalformedURLException e ) {
145                log.error( "exception (" + e.getMessage() + ") while setting up classloaders for external jar:" + externaljar + ", continuing without external jars." );
146            }
147        }
148        
149        if( i == 0 ) {
150            log.error( "all external jars threw an exception while setting up classloaders for them, continuing with standard classloading. " + 
151                       "See https://jspwiki-wiki.apache.org/Wiki.jsp?page=InstallingPlugins for help on how to install custom plugins." );
152            return ClassUtil.class.getClassLoader();
153        }
154        
155        return new URLClassLoader(urls, ClassUtil.class.getClassLoader());
156    }
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    public static Class< ? > findClass( final String packageName, final String className ) throws ClassNotFoundException {
168        try {
169            return ClassUtil.class.getClassLoader().loadClass( className );
170        } catch( final ClassNotFoundException e ) {
171            return ClassUtil.class.getClassLoader().loadClass( packageName + "." + className );
172        }
173    }
174    
175    /**
176     * Lists all the files in classpath under a given package.
177     * 
178     * @param rootPackage the base package. Can be {code null}.
179     * @return all files entries in classpath under the given package
180     */
181    public static List< String > classpathEntriesUnder( final String rootPackage ) {
182        final List< String > results = new ArrayList<>();
183        Enumeration< URL > en = null;
184        if( StringUtils.isNotEmpty( rootPackage ) ) {
185            try {
186                en = ClassUtil.class.getClassLoader().getResources( rootPackage );
187            } catch( final IOException e ) {
188                log.error( e.getMessage(), e );
189            }
190        }
191        
192        while( en != null && en.hasMoreElements() ) {
193            final URL url = en.nextElement();
194            try {
195                if( "jar".equals( url.getProtocol() ) ) {
196                    jarEntriesUnder( results, ( JarURLConnection )url.openConnection(), rootPackage );
197                } else if( "file".equals( url.getProtocol() ) ) {
198                    fileEntriesUnder( results, new File( url.getFile() ), rootPackage );
199                }
200                
201            } catch( final IOException ioe ) {
202                log.error( ioe.getMessage(), ioe );
203            }
204        }
205        return results;
206    }
207    
208    /**
209     * Searchs for all the files in classpath under a given package, for a given {@link File}. If the 
210     * {@link File} is a directory all files inside it are stored, otherwise the {@link File} itself is
211     * stored
212     * 
213     * @param results collection in which the found entries are stored
214     * @param file given {@link File} to search in.
215     * @param rootPackage base package.
216     */
217    static void fileEntriesUnder( final List< String > results, final File file, final String rootPackage ) {
218        log.debug( "scanning [" + file.getName() + "]" );
219        if( file.isDirectory() ) {
220            final Iterator< File > files = FileUtils.iterateFiles( file, null, true );
221            while( files.hasNext() ) {
222                final File subfile = files.next();
223                // store an entry similar to the jarSearch(..) below ones
224                final String entry = StringUtils.replace( subfile.getAbsolutePath(), file.getAbsolutePath() + File.separatorChar, StringUtils.EMPTY );
225                results.add( rootPackage + "/" + entry );
226            }
227        } else {
228            results.add( file.getName() );
229        }
230    }
231    
232    /**
233     * Searchs for all the files in classpath under a given package, for a given {@link JarURLConnection}.
234     * 
235     * @param results collection in which the found entries are stored
236     * @param jurlcon given {@link JarURLConnection} to search in.
237     * @param rootPackage base package.
238     */
239    static void jarEntriesUnder( final List< String > results, final JarURLConnection jurlcon, final String rootPackage ) {
240        try( final JarFile jar = jurlcon.getJarFile() ) {
241            log.debug( "scanning [" + jar.getName() +"]" );
242            final Enumeration< JarEntry > entries = jar.entries();
243            while( entries.hasMoreElements() ) {
244                final JarEntry entry = entries.nextElement();
245                if( entry.getName().startsWith( rootPackage ) && !entry.isDirectory() ) {
246                    results.add( entry.getName() );
247                }
248            }
249        } catch( final IOException ioe ) {
250            log.error( ioe.getMessage(), ioe );
251        }
252    }
253    
254    /**
255     *  This method is used to locate and instantiate a mapped class.
256     *  You may redefine anything in the resource file which is located in your classpath
257     *  under the name <code>ClassUtil.MAPPINGS ({@value #MAPPINGS})</code>.
258     *  <p>
259     *  This is an extremely powerful system, which allows you to remap many of
260     *  the JSPWiki core classes to your own class.  Please read the documentation
261     *  included in the default <code>{@value #MAPPINGS}</code> file to see
262     *  how this method works. 
263     *  
264     *  @param requestedClass The name of the class you wish to instantiate.
265     *  @return An instantiated Object.
266     *  @throws IllegalArgumentException If the class cannot be found or instantiated. 
267     *  @throws ReflectiveOperationException If the class cannot be found or instantiated.
268     *  @since 2.5.40
269     */
270    @SuppressWarnings("unchecked")
271    public static < T > T getMappedObject( final String requestedClass ) throws ReflectiveOperationException, IllegalArgumentException {
272        final Object[] initargs = {};
273        return ( T )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        final Constructor< ? >[] ctors = cl.getConstructors();
300        
301        //  Try to find the proper constructor by comparing the initargs array classes and the constructor types.
302        for( final Constructor< ? > ctor : ctors ) {
303            final Class< ? >[] params = ctor.getParameterTypes();
304            if( params.length == initargs.length ) {
305                for( int arg = 0; arg < initargs.length; arg++ ) {
306                    if( params[ arg ].isAssignableFrom( initargs[ arg ].getClass() ) ) {
307                        //  Ha, found it!  Instantiating and returning...
308                        return ( T )ctor.newInstance( initargs );
309                    }
310                }
311            }
312        }
313        
314        //  No arguments, so we can just call a default constructor and ignore the arguments.
315        return ( T )cl.getDeclaredConstructor().newInstance();
316    }
317
318    /**
319     *  Finds a mapped class from the c_classMappings list.  If there is no mappped class, will use the requestedClass.
320     *  
321     *  @param requestedClass requested class.
322     *  @return A Class object which you can then instantiate.
323     *  @throws ClassNotFoundException if the class is not found.
324     */
325    public static Class< ? > getMappedClass( final String requestedClass ) throws ClassNotFoundException {
326        String mappedClass = c_classMappings.get( requestedClass );
327        if( mappedClass == null ) {
328            mappedClass = requestedClass;
329        }
330        
331        return Class.forName( mappedClass );
332    }
333    
334    /**
335     * checks if {@code srcClassName} is a subclass of {@code parentClassname}.
336     * 
337     * @param srcClassName expected subclass.
338     * @param parentClassName expected parent class.
339     * @return {@code true} if {@code srcClassName} is a subclass of {@code parentClassname}, {@code false} otherwise.
340     */
341    public static boolean assignable( final String srcClassName, final String parentClassName ) {
342        try {
343            final Class< ? > src = Class.forName( srcClassName );
344            final Class< ? > parent = Class.forName( parentClassName );
345            return parent.isAssignableFrom( src );
346        } catch( final Exception e ) {
347            log.error( e.getMessage(), e );
348        }
349        return false;
350    }
351
352    /**
353     * Checks if a given class exists in classpath.
354     *
355     * @param className the class to check for existence.
356     * @return {@code true} if it exists, {@code false} otherwise.
357     */
358    public static boolean exists( final String className ) {
359        try {
360            Class.forName( className, false, ClassUtil.class.getClassLoader() );
361            return true;
362        } catch( final ClassNotFoundException e ) {
363            return false;
364        }
365    }
366
367    public static Map< String, String > getExtraClassMappings() {
368        return c_classMappingsExtra;
369    }
370    
371}