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