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