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     */
019    package org.apache.wiki.util;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.lang.reflect.Constructor;
024    import java.lang.reflect.InvocationTargetException;
025    import java.net.JarURLConnection;
026    import java.net.MalformedURLException;
027    import java.net.URL;
028    import java.net.URLClassLoader;
029    import java.util.ArrayList;
030    import java.util.Enumeration;
031    import java.util.Hashtable;
032    import java.util.Iterator;
033    import java.util.List;
034    import java.util.Map;
035    import java.util.jar.JarEntry;
036    import java.util.jar.JarFile;
037    
038    import org.apache.commons.io.FileUtils;
039    import org.apache.commons.lang.StringUtils;
040    import org.apache.log4j.Logger;
041    import org.apache.wiki.api.engine.PluginManager;
042    import org.apache.wiki.api.exceptions.WikiException;
043    import 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     */
051    public 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    }