xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginFramework.java (revision 5004021a2199f94c52cadc424b07f55fbee15fd0)
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