1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. 22 */ 23 package org.opengrok.indexer.framework; 24 25 import java.io.File; 26 import java.io.IOException; 27 import java.lang.reflect.InvocationTargetException; 28 import java.lang.reflect.Modifier; 29 import java.security.AccessController; 30 import java.security.PrivilegedAction; 31 import java.util.Arrays; 32 import java.util.Enumeration; 33 import java.util.LinkedList; 34 import java.util.List; 35 import java.util.jar.JarEntry; 36 import java.util.jar.JarFile; 37 import java.util.logging.Level; 38 import java.util.logging.Logger; 39 40 import org.jetbrains.annotations.Nullable; 41 import org.opengrok.indexer.authorization.IAuthorizationPlugin; 42 import org.opengrok.indexer.logger.LoggerFactory; 43 import org.opengrok.indexer.util.IOUtils; 44 45 /** 46 * Plugin framework for plugins of type {@code PluginType}. 47 * 48 * @author Krystof Tulinger 49 */ 50 public abstract class PluginFramework<PluginType> { 51 52 private static final Logger LOGGER = LoggerFactory.getLogger(PluginFramework.class); 53 54 private static final String CLASS_SUFFIX = ".class"; 55 56 /** 57 * Class of the plugin type, necessary for instantiating and searching. 58 */ 59 private final Class<PluginType> classType; 60 61 /** 62 * Plugin directory. 63 */ 64 private File pluginDirectory; 65 66 /** 67 * Customized class loader for plugin classes. 68 */ 69 private PluginClassLoader loader; 70 71 /** 72 * Whether to load plugins from class files and jar files. 73 */ 74 private boolean loadClasses = true; 75 private boolean loadJars = true; 76 77 /** 78 * Create a new instance of plugin framework for a plugin directory. 79 * 80 * @param classType the class of the plugin type 81 * @param path the plugin directory path 82 */ PluginFramework(Class<PluginType> classType, String path)83 protected PluginFramework(Class<PluginType> classType, String path) { 84 this.classType = classType; 85 setPluginDirectory(path); 86 } 87 88 /** 89 * Get the plugin directory. 90 * 91 * @return plugin directory file 92 */ getPluginDirectory()93 public synchronized File getPluginDirectory() { 94 return pluginDirectory; 95 } 96 97 /** 98 * Set the plugin directory. 99 * 100 * @param pluginDirectory the directory 101 */ setPluginDirectory(File pluginDirectory)102 public synchronized void setPluginDirectory(File pluginDirectory) { 103 this.pluginDirectory = pluginDirectory; 104 } 105 106 /** 107 * Set the plugin directory. 108 * 109 * @param directory the directory path 110 */ setPluginDirectory(String directory)111 public synchronized void setPluginDirectory(String directory) { 112 setPluginDirectory(directory != null ? new File(directory) : null); 113 } 114 115 /** 116 * Make {@code reload()} search for plugins in class files. 117 * 118 * @param flag true or false 119 */ setLoadClasses(boolean flag)120 public void setLoadClasses(boolean flag) { 121 loadClasses = flag; 122 } 123 124 /** 125 * Whether to search for plugins in class files. 126 * 127 * @return true if enabled, false otherwise 128 */ isLoadClassesEnabled()129 public boolean isLoadClassesEnabled() { 130 return loadClasses; 131 } 132 133 /** 134 * Make {@code reload()} search for plugins in jar files. 135 * 136 * @param flag true or false 137 */ setLoadJars(boolean flag)138 public void setLoadJars(boolean flag) { 139 loadJars = flag; 140 } 141 142 /** 143 * Whether to search for plugins in class files. 144 * 145 * @return true if enabled, false otherwise 146 */ isLoadJarsEnabled()147 public boolean isLoadJarsEnabled() { 148 return loadJars; 149 } 150 151 /** 152 * Return the java canonical name for the plugin class. If the canonical 153 * name does not exist it returns the usual java name. 154 * 155 * @param plugin the plugin 156 * @return the class name 157 */ getClassName(IAuthorizationPlugin plugin)158 protected String getClassName(IAuthorizationPlugin plugin) { 159 if (plugin.getClass().getCanonicalName() != null) { 160 return plugin.getClass().getCanonicalName(); 161 } 162 return plugin.getClass().getName(); 163 } 164 165 /** 166 * Wrapper around the class loading. Report all exceptions into the log. 167 * 168 * @param classname full name of the class 169 * @return the class implementing the {@link IAuthorizationPlugin} interface 170 * or null if there is no such class 171 * @see #loadClass(String) 172 */ handleLoadClass(String classname)173 public PluginType handleLoadClass(String classname) { 174 try { 175 return loadClass(classname); 176 } catch (ClassNotFoundException ex) { 177 LOGGER.log(Level.WARNING, String.format("Class \"%s\" was not found", classname), ex); 178 } catch (SecurityException ex) { 179 LOGGER.log(Level.WARNING, String.format("Class \"%s\" was found but it is placed in prohibited package: ", classname), ex); 180 } catch (InstantiationException ex) { 181 LOGGER.log(Level.WARNING, String.format("Class \"%s\" could not be instantiated: ", classname), ex); 182 } catch (IllegalAccessException ex) { 183 LOGGER.log(Level.WARNING, String.format("Class \"%s\" loader threw an exception: ", classname), ex); 184 } catch (Throwable ex) { 185 LOGGER.log(Level.WARNING, String.format("Class \"%s\" loader threw an unknown error: ", classname), ex); 186 } 187 return null; 188 } 189 190 /** 191 * Load a class into JVM with custom class loader. Call a non-parametric 192 * constructor to create a new instance of that class. 193 * <p> 194 * <p>The classes implementing/extending the {@code PluginType} type are 195 * returned and initialized with a call to a non-parametric constructor. 196 * 197 * @param classname the full name of the class to load 198 * @return the class implementing/extending the {@code PluginType} class 199 * or null if there is no such class 200 * @throws ClassNotFoundException when the class can not be found 201 * @throws SecurityException when it is prohibited to load such class 202 * @throws InstantiationException when it is impossible to create a new 203 * instance of that class 204 * @throws IllegalAccessException when the constructor of the class is not 205 * accessible 206 * @throws NoSuchMethodException when the class does not have no-argument constructor 207 * @throws InvocationTargetException if the underlying constructor of the class throws an exception 208 */ 209 @SuppressWarnings({"unchecked"}) loadClass(String classname)210 private PluginType loadClass(String classname) throws ClassNotFoundException, 211 SecurityException, 212 InstantiationException, 213 IllegalAccessException, 214 NoSuchMethodException, 215 InvocationTargetException { 216 217 Class<?> c = loader.loadClass(classname); 218 219 // check for implemented interfaces or extended superclasses 220 for (Class<?> intf1 : getSuperclassesAndInterfaces(c)) { 221 if (intf1.getCanonicalName().equals(classType.getCanonicalName()) 222 && !Modifier.isAbstract(c.getModifiers())) { 223 // call to non-parametric constructor 224 return (PluginType) c.getDeclaredConstructor().newInstance(); 225 } 226 } 227 LOGGER.log(Level.FINEST, "Plugin class \"{0}\" does not implement IAuthorizationPlugin interface.", classname); 228 return null; 229 } 230 231 /** 232 * Get all available interfaces or superclasses of a class clazz. 233 * 234 * @param clazz class 235 * @return list of interfaces or superclasses of the class clazz 236 */ getSuperclassesAndInterfaces(Class<?> clazz)237 protected List<Class<?>> getSuperclassesAndInterfaces(Class<?> clazz) { 238 List<Class<?>> types = new LinkedList<>(); 239 Class<?> self = clazz; 240 while (self != null && self != classType && !types.contains(classType)) { 241 types.add(self); 242 types.addAll(Arrays.asList(self.getInterfaces())); 243 self = self.getSuperclass(); 244 } 245 return types; 246 } 247 248 /** 249 * Traverse list of files which possibly contain a java class 250 * to load all classes. 251 * Each class is loaded with {@link #handleLoadClass(String)} which 252 * delegates the loading to the custom class loader 253 * {@link #loadClass(String)}. 254 * 255 * @param fileList list of files which possibly contain a java class 256 * @see #handleLoadClass(String) 257 * @see #loadClass(String) 258 */ loadClassFiles(List<File> fileList)259 private void loadClassFiles(List<File> fileList) { 260 PluginType plugin; 261 262 for (File file : fileList) { 263 String classname = getClassName(file); 264 if (classname == null || classname.isEmpty()) { 265 continue; 266 } 267 // Load the class in memory and try to find a configured space for this class. 268 if ((plugin = handleLoadClass(classname)) != null) { 269 classLoaded(plugin); 270 } 271 } 272 } 273 274 /** 275 * Traverse list of jar files to load all classes. 276 * <p> 277 * Each class is loaded with {@link #handleLoadClass(String)} which 278 * delegates the loading to the custom class loader 279 * {@link #loadClass(String)}. 280 * 281 * @param fileList list of jar files containing java classes 282 * @see #handleLoadClass(String) 283 * @see #loadClass(String) 284 */ loadJarFiles(List<File> fileList)285 private void loadJarFiles(List<File> fileList) { 286 PluginType pf; 287 288 for (File file : fileList) { 289 try (JarFile jar = new JarFile(file)) { 290 Enumeration<JarEntry> entries = jar.entries(); 291 while (entries.hasMoreElements()) { 292 JarEntry entry = entries.nextElement(); 293 String classname = getClassName(entry); 294 if (classname == null || classname.isEmpty()) { 295 continue; 296 } 297 // Load the class in memory and try to find a configured space for this class. 298 if ((pf = handleLoadClass(classname)) != null) { 299 classLoaded(pf); 300 } 301 } 302 } catch (IOException ex) { 303 LOGGER.log(Level.WARNING, "Could not manipulate with file because of: ", ex); 304 } 305 } 306 } 307 308 @Nullable getClassName(File file)309 private String getClassName(File file) { 310 if (!file.getName().endsWith(CLASS_SUFFIX)) { 311 return null; 312 } 313 314 String classname = file.getAbsolutePath().substring(pluginDirectory.getAbsolutePath().length() + 1); 315 classname = classname.replace(File.separatorChar, '.'); // convert to package name 316 // no need to check for the index from lastIndexOf because we're in a branch 317 // where we expect the .class suffix 318 classname = classname.substring(0, classname.lastIndexOf('.')); // strip .class 319 return classname; 320 } 321 322 // The filePath is checked not to point outside the "target" directory in the code below. 323 @SuppressWarnings("javasecurity:S6096") 324 @Nullable getClassName(JarEntry jarEntry)325 private String getClassName(JarEntry jarEntry) { 326 final String filePath = jarEntry.getName(); 327 if (!filePath.endsWith(CLASS_SUFFIX)) { 328 return null; 329 } 330 331 File file = new File(pluginDirectory.getAbsolutePath(), filePath); 332 try { 333 if (!file.getCanonicalPath().startsWith(pluginDirectory.getCanonicalPath() + File.separator)) { 334 LOGGER.log(Level.WARNING, "canonical path for jar entry {0} leads outside the origin", filePath); 335 return null; 336 } 337 } catch (IOException e) { 338 LOGGER.log(Level.WARNING, "failed to get canonical path for {0}", file); 339 return null; 340 } 341 342 // java jar always uses / as separator 343 String classname = filePath.replace('/', '.'); // convert to package name 344 return classname.substring(0, classname.lastIndexOf('.')); // strip .class 345 } 346 347 /** 348 * Allow the implementing class to interact with the loaded class when the class 349 * was loaded with the custom class loader. 350 * 351 * @param plugin the loaded plugin 352 */ classLoaded(PluginType plugin)353 protected abstract void classLoaded(PluginType plugin); 354 355 356 /** 357 * Perform custom operations before the plugins are loaded. 358 */ beforeReload()359 protected abstract void beforeReload(); 360 361 /** 362 * Perform custom operations when the framework has reloaded all available plugins. 363 * <p> 364 * When this is invoked, all plugins has been loaded into the memory and for each available plugin 365 * the {@link #classLoaded(Object)} was invoked. 366 */ afterReload()367 protected abstract void afterReload(); 368 369 /** 370 * Calling this function forces the framework to reload the plugins. 371 * <p> 372 * <p>Plugins are taken from the pluginDirectory. 373 */ reload()374 public final void reload() { 375 if (pluginDirectory == null || !pluginDirectory.isDirectory() || !pluginDirectory.canRead()) { 376 LOGGER.log(Level.WARNING, "Plugin directory not found or not readable: {0}. " 377 + "All requests allowed.", pluginDirectory); 378 return; 379 } 380 381 LOGGER.log(Level.INFO, "Plugins are being reloaded from {0}", pluginDirectory.getAbsolutePath()); 382 383 // trashing out the old instance of the loaded enables us 384 // to reload the stack at runtime 385 loader = AccessController.doPrivileged((PrivilegedAction<PluginClassLoader>) () -> new PluginClassLoader(pluginDirectory)); 386 387 // notify the implementing class that the reload is about to begin 388 beforeReload(); 389 390 // load all other possible plugin classes. 391 if (isLoadClassesEnabled()) { 392 loadClassFiles(IOUtils.listFilesRec(pluginDirectory, CLASS_SUFFIX)); 393 } 394 if (isLoadJarsEnabled()) { 395 loadJarFiles(IOUtils.listFiles(pluginDirectory, ".jar")); 396 } 397 398 // notify the implementing class that the reload has ended 399 afterReload(); 400 } 401 } 402