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
053     *  in the classpath.  It's value is "{@value}".
054     */
055    public  static final String MAPPINGS = "ini/classmappings.xml";
056    
057    private static Map<String, String> c_classMappings = new ConcurrentHashMap<>();
058
059    private static boolean classLoaderSetup = false;
060    private static ClassLoader loader = null;
061
062
063    /**
064     *  Initialize the class mappings document.
065     */
066    static {
067        List< Element > nodes = XmlUtil.parse( MAPPINGS, "/classmappings/mapping" );
068
069        if( nodes.size() > 0 ) {
070            for( Iterator< Element > i = nodes.iterator(); i.hasNext(); ) {
071                Element f = i.next();
072            
073                String key = f.getChildText("requestedClass");
074                String className = f.getChildText("mappedClass");
075                
076                c_classMappings.put( key, className );
077                
078                log.debug("Mapped class '"+key+"' to class '"+className+"'");
079            }
080        } else {
081            log.info("Didn't find class mapping document in "+MAPPINGS);
082        }
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
101     *          from the list.
102     */
103    public static Class<?> findClass( List< String > packages,  List< String > externaljars, String className ) throws ClassNotFoundException {
104        if (!classLoaderSetup) {
105            loader = setupClassLoader(externaljars);
106        }
107
108        try {
109            return loader.loadClass( className );
110        } catch( ClassNotFoundException e ) {
111            for( Iterator< String > i = packages.iterator(); i.hasNext(); ) {
112                String packageName = i.next();
113                try {
114                    return loader.loadClass( packageName + "." + className );
115                } catch( ClassNotFoundException ex ) {
116                    // This is okay, we go to the next package.
117                }
118            }
119
120        }
121
122        throw new ClassNotFoundException( "Class '" + className + "' not found in search path!" );
123    }
124
125    /**
126     * Setup the plugin classloader, checking if there are external JARS to add.
127     * 
128     * @param externaljars external jars to load into the classloader.
129     * @return the classloader that can load classes from the configured external jars or, if not specified, the classloader that loaded this class.
130     */
131    private static ClassLoader setupClassLoader(List<String> externaljars) {
132        classLoaderSetup = true;
133        log.info("setting up classloaders for external (plugin) jars");
134        if (externaljars.size() == 0) {
135            log.info("no external jars configured, using standard classloading");
136            return ClassUtil.class.getClassLoader();
137        }
138        URL[] urls = new URL[externaljars.size()];
139        int i = 0;
140        for( String externaljar : externaljars ) {
141            try {
142                File jarFile = new File( externaljar );
143                URL ucl = jarFile.toURI().toURL();
144                urls[i++] = ucl;
145                log.info("added " + ucl + " to list of external jars");
146            } catch (MalformedURLException e) {
147                log.error("exception (" + e.getMessage() +") while setting up classloaders for external jar:" + externaljar + ", continuing without external jars.");
148            }
149        }
150        
151        if( i == 0 ) {
152            log.error( "all external jars threw an exception while setting up classloaders for them, continuing with standard classloading. " + 
153                       "See https://jspwiki-wiki.apache.org/Wiki.jsp?page=InstallingPlugins for help on how to install custom plugins." );
154            return ClassUtil.class.getClassLoader();
155        }
156        
157        return new URLClassLoader(urls, ClassUtil.class.getClassLoader());
158    }
159
160    /**
161     *
162     *  It will first attempt to instantiate the class directly from the className,
163     *  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
171    public static Class<?> findClass(String packageName, String className) throws ClassNotFoundException {
172        try {
173            return ClassUtil.class.getClassLoader().loadClass(className);
174        } catch (ClassNotFoundException e) {
175            return 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    {
187        List< String > results = new ArrayList< >();
188        Enumeration< URL > en = null;
189        if( StringUtils.isNotEmpty( rootPackage ) ) {
190            try
191            {
192                en = ClassUtil.class.getClassLoader().getResources( rootPackage );
193            }
194            catch( IOException e )
195            {
196                log.error( e.getMessage(), e );
197            }
198        }
199        
200        while( en != null && en.hasMoreElements() )
201        {
202            URL url = en.nextElement();
203            try
204            {
205                if( "jar".equals( url.getProtocol() ) ) 
206                {
207                    jarEntriesUnder( results, ( JarURLConnection )url.openConnection(), rootPackage );
208                } 
209                else if( "file".equals( url.getProtocol() ) ) 
210                {
211                    fileEntriesUnder( results, new File( url.getFile() ), rootPackage );
212                }
213                
214            }
215            catch (IOException ioe)
216            {
217                log.error( ioe.getMessage(), ioe );
218            }
219        }
220        return results;
221    }
222    
223    /**
224     * Searchs for all the files in classpath under a given package, for a given {@link File}. If the 
225     * {@link File} is a directory all files inside it are stored, otherwise the {@link File} itself is
226     * stored
227     * 
228     * @param results collection in which the found entries are stored
229     * @param file given {@link File} to search in.
230     * @param rootPackage base package.
231     */
232    static void fileEntriesUnder( List< String > results, File file, String rootPackage ) 
233    {
234        log.debug( "scanning [" + file.getName() +"]" );
235        if( file.isDirectory() ) {
236            Iterator< File > files = FileUtils.iterateFiles( file, null, true );
237            while( files.hasNext() ) 
238            {
239                File subfile = files.next();
240                // store an entry similar to the jarSearch(..) below ones
241                String entry = StringUtils.replace( subfile.getAbsolutePath(), file.getAbsolutePath() + File.separatorChar, StringUtils.EMPTY );
242                results.add( rootPackage + "/" + entry );
243            }
244        } else {
245            results.add( file.getName() );
246        }
247    }
248    
249    /**
250     * Searchs for all the files in classpath under a given package, for a given {@link JarURLConnection}.
251     * 
252     * @param results collection in which the found entries are stored
253     * @param jurlcon given {@link JarURLConnection} to search in.
254     * @param rootPackage base package.
255     */
256    static void jarEntriesUnder( List< String > results, JarURLConnection jurlcon, String rootPackage ) {
257        try( JarFile jar = jurlcon.getJarFile() ) {
258            log.debug( "scanning [" + jar.getName() +"]" );
259            Enumeration< JarEntry > entries = jar.entries();
260            while( entries.hasMoreElements() ) {
261                JarEntry entry = entries.nextElement();
262                if( entry.getName().startsWith( rootPackage ) && !entry.isDirectory() ) {
263                    results.add( entry.getName() );
264                }
265            }
266        } catch( IOException ioe ) {
267            log.error( ioe.getMessage(), ioe );
268        }
269    }
270    
271    /**
272     *  This method is used to locate and instantiate a mapped class.
273     *  You may redefine anything in the resource file which is located in your classpath
274     *  under the name <code>ClassUtil.MAPPINGS ({@value #MAPPINGS})</code>.
275     *  <p>
276     *  This is an extremely powerful system, which allows you to remap many of
277     *  the JSPWiki core classes to your own class.  Please read the documentation
278     *  included in the default <code>{@value #MAPPINGS}</code> file to see
279     *  how this method works. 
280     *  
281     *  @param requestedClass The name of the class you wish to instantiate.
282     *  @return An instantiated Object.
283     *  @throws IllegalArgumentException If the class cannot be found or instantiated. 
284     *  @throws ReflectiveOperationException If the class cannot be found or instantiated.
285     *  @since 2.5.40
286     */
287    @SuppressWarnings("unchecked")
288    public static < T > T getMappedObject( String requestedClass )
289        throws ReflectiveOperationException, IllegalArgumentException
290    {
291        Object[] initargs = {};
292        return ( T )getMappedObject(requestedClass, initargs );
293    }
294
295    /**
296     *  This method is used to locate and instantiate a mapped class.
297     *  You may redefine anything in the resource file which is located in your classpath
298     *  under the name <code>{@value #MAPPINGS}</code>.
299     *  <p>
300     *  This is an extremely powerful system, which allows you to remap many of
301     *  the JSPWiki core classes to your own class.  Please read the documentation
302     *  included in the default <code>{@value #MAPPINGS}</code> file to see
303     *  how this method works. 
304     *  <p>
305     *  This method takes in an object array for the constructor arguments for classes
306     *  which have more than two constructors.
307     *  
308     *  @param requestedClass The name of the class you wish to instantiate.
309     *  @param initargs The parameters to be passed to the constructor. May be <code>null</code>.
310     *  @return An instantiated Object.
311     *  @throws IllegalArgumentException If the class cannot be found or instantiated. 
312     *  @throws ReflectiveOperationException If the class cannot be found or instantiated.
313     *  @since 2.5.40
314     */
315    @SuppressWarnings( "unchecked" )
316    public static < T > T getMappedObject( String requestedClass, Object... initargs )
317        throws ReflectiveOperationException, IllegalArgumentException
318    {
319        Class<?> cl = getMappedClass( requestedClass );
320        Constructor<?>[] ctors = cl.getConstructors();
321        
322        //
323        //  Try to find the proper constructor by comparing the
324        //  initargs array classes and the constructor types.
325        //
326        for( int c = 0; c < ctors.length; c++ )
327        {
328            Class<?>[] params = ctors[c].getParameterTypes();
329            
330            if( params.length == initargs.length )
331            {
332                for( int arg = 0; arg < initargs.length; arg++ )
333                {
334                    if( params[arg].isAssignableFrom(initargs[arg].getClass()))
335                    {
336                        //
337                        //  Ha, found it!  Instantiating and returning...
338                        //
339                        return ( T )ctors[c].newInstance(initargs);
340                    }
341                }
342            }
343        }
344        
345        //
346        //  No arguments, so we can just call a default constructor and ignore the arguments.
347        //
348        return ( T )cl.getDeclaredConstructor().newInstance();
349    }
350
351    /**
352     *  Finds a mapped class from the c_classMappings list.  If there is no
353     *  mappped class, will use the requestedClass.
354     *  
355     *  @param requestedClass
356     *  @return A Class object which you can then instantiate.
357     *  @throws ClassNotFoundException
358     */
359    public static Class< ? > getMappedClass( String requestedClass ) throws ClassNotFoundException {
360        String mappedClass = c_classMappings.get( requestedClass );
361        
362        if( mappedClass == null )
363        {
364            mappedClass = requestedClass;
365        }
366        
367        Class< ? > cl = Class.forName(mappedClass);
368        
369        return cl;
370    }
371    
372    /**
373     * checks if {@code srcClassName} is a subclass of {@code parentClassname}.
374     * 
375     * @param srcClassName expected subclass.
376     * @param parentClassName expected parent class.
377     * @return {@code true} if {@code srcClassName} is a subclass of {@code parentClassname}, {@code false} otherwise.
378     */
379    public static boolean assignable( String srcClassName, String parentClassName ) {
380        try {
381            Class< ? > src = Class.forName( srcClassName );
382            Class< ? > parent = Class.forName( parentClassName );
383            return parent.isAssignableFrom( src );
384        } catch( Exception e ) {
385            log.error( e.getMessage(), e );
386        }
387        return false;
388    }
389    
390}