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}