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}