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