View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *  http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.any23.plugin;
19  
20  import org.apache.any23.cli.Tool;
21  import org.apache.any23.configuration.DefaultConfiguration;
22  import org.apache.any23.extractor.ExtractorFactory;
23  import org.apache.any23.extractor.ExtractorGroup;
24  import org.apache.any23.extractor.ExtractorRegistry;
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import java.io.File;
29  import java.io.FilenameFilter;
30  import java.io.IOException;
31  import java.net.MalformedURLException;
32  import java.net.URL;
33  import java.net.URLClassLoader;
34  import java.util.ArrayList;
35  import java.util.HashSet;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.ServiceLoader;
39  import java.util.Set;
40  
41  /**
42   * The <i>Any23PluginManager</i> is responsible for inspecting
43   * dynamically the classpath and retrieving useful classes.
44   *
45   * @author Michele Mostarda (mostarda@fbk.eu)
46   */
47  public class Any23PluginManager {
48  
49      /**
50       * Any23 Command Line Interface package.
51       */
52      public static final String CLI_PACKAGE = Tool.class.getPackage().getName();
53  
54      /**
55       * Property where look for plugins.
56       */
57      public static final String PLUGIN_DIRS_PROPERTY = "any23.plugin.dirs";
58  
59      /**
60       * List separator for the string declaring the plugin list.
61       */
62      public static final String PLUGIN_DIRS_LIST_SEPARATOR = ":";
63  
64      /**
65       * Internal logger.
66       */
67      private static final Logger logger = LoggerFactory.getLogger(Any23PluginManager.class);
68  
69      /**
70       * Singleton lazy instance.
71       */
72      private static final Any23PluginManager.html#Any23PluginManager">Any23PluginManager instance = new Any23PluginManager();
73  
74      /**
75       * Internal class loader used to dynamically load classes.
76       */
77      private final DynamicClassLoader dynamicClassLoader;
78  
79      /**
80       * Constructor.
81       */
82      private Any23PluginManager() {
83          dynamicClassLoader = new DynamicClassLoader();
84      }
85  
86      /**
87       * @return a singleton instance of {@link Any23PluginManager}.
88       */
89      public static synchronized Any23PluginManager getInstance() {
90          return instance;
91      }
92  
93      /**
94       * Loads a <i>JAR</i> file in the classpath.
95       *
96       * @param jar the JAR file to be loaded.
97       * @return <code>true</code> if the JAR is added for the first time to the classpath,
98       *         <code>false</code> otherwise.
99       */
100     public synchronized boolean loadJAR(File jar) {
101         if(jar == null) {
102             throw new NullPointerException("jar file cannot be null.");
103         }
104         if (!jar.isFile() && !jar.exists()) {
105             throw new IllegalArgumentException(
106                     String.format("Invalid JAR [%s], must be an existing file.", jar.getAbsolutePath())
107             );
108         }
109         return dynamicClassLoader.addJAR(jar);
110     }
111 
112     /**
113      * Loads a list of <i>JAR</i>s in the classpath.
114      *
115      * @param jars list of JARs to be loaded.
116      * @return list of exceptions raised during the loading.
117      */
118     public synchronized Throwable[] loadJARs(File... jars) {
119         final List<Throwable> result = new ArrayList<>();
120         for (File jar : jars) {
121             try {
122                 loadJAR(jar);
123             } catch (Throwable t) {
124                 result.add(
125                         new IllegalArgumentException(
126                                 String.format("Error while loading jar [%s]", jar.getAbsolutePath()),
127                                 t
128                         )
129                 );
130             }
131         }
132         return result.toArray(new Throwable[result.size()]);
133     }
134 
135     /**
136      * Loads a <i>classes</i> directory in the classpath.
137      *
138      * @param classDir the directory to be loaded.
139      * @return <code>true</code> if the directory is added for the first time to the classpath,
140      *         <code>false</code> otherwise.
141      */
142     public synchronized boolean loadClassDir(File classDir) {
143         if(classDir == null) {
144           throw new NullPointerException("classDir cannot be null.");
145         }
146         if (!classDir.isDirectory() && !classDir.exists()) {
147             throw new IllegalArgumentException(
148                     String.format("Invalid class dir [%s], must be an existing file.", classDir.getAbsolutePath())
149             );
150         }
151         return dynamicClassLoader.addClassDir(classDir);
152     }
153 
154     /**
155      * Loads a list of class dirs in the classpath.
156      *
157      * @param classDirs list of class dirs to be loaded.
158      * @return  list of exceptions raised during the loading.
159      */
160     public synchronized Throwable[] loadClassDirs(File... classDirs) {
161         final List<Throwable> result = new ArrayList<>();
162         for (File classDir : classDirs) {
163             try {
164                 loadClassDir(classDir);
165             } catch (Throwable t) {
166                 result.add(
167                         new IllegalArgumentException(
168                                 String.format("Error while loading class dir [%s]", classDir.getAbsolutePath()),
169                                 t
170                         )
171                 );
172             }
173         }
174         return result.toArray(new Throwable[result.size()]);
175     }
176 
177     /**
178      * Loads all the JARs detected in a given directory.
179      *
180      * @param jarDir directory containing the JARs to be loaded.
181      *     Example '/usr/local/apache-tomcat-7.0.72/webapps/apache-any23-service-2.2-SNAPSHOT/WEB-INF/lib/apache-any23-openie'
182      * @return <code>true</code> if all JARs in dir are loaded.
183      */
184     public synchronized boolean loadJARDir(File jarDir) {
185         if(jarDir == null)
186             throw new NullPointerException("JAR dir must be not null.");
187         if(!jarDir.exists() )
188             throw new IllegalArgumentException("Given directory doesn't exist:" + jarDir.getAbsolutePath());
189         if(!jarDir.isDirectory() )
190             throw new IllegalArgumentException(
191                     "given file exists and it is not a directory: " + jarDir.getAbsolutePath()
192             );
193         boolean loaded = true;
194         for (File jarFile : jarDir.listFiles(
195                 new FilenameFilter() {
196                     @Override
197                     public boolean accept(File dir, String name) {
198                         return name.endsWith(".jar");
199                     }
200                 })
201         ) {
202             loaded &= loadJAR(jarFile);
203         }
204         return loaded;
205     }
206 
207     /**
208      * Loads a generic list of files, trying to determine the type of every file.
209      *
210      * @param files list of files to be loaded.
211      * @return list of errors occurred during loading.
212      */
213     public synchronized Throwable[] loadFiles(File... files) {
214         final List<Throwable> errors = new ArrayList<>();
215         for(File file : files) {
216             try {
217                 if (file.isFile() && file.getName().endsWith(".jar")) {
218                     loadJAR(file);
219                 } else if (file.isDirectory()) {
220                     if (file.getName().endsWith("classes")) {
221                         loadClassDir(file);
222                     } else {
223                         loadJARDir(file);
224                     }
225                 } else {
226                     throw new IllegalArgumentException("Cannot handle file " + file.getAbsolutePath());
227                 }
228             } catch (Throwable t) {
229                 errors.add(t);
230             }
231         }
232         return errors.toArray(new Throwable[errors.size()]);
233     }
234 
235     /**
236      * Returns all classes within the specified <code>packageName</code> satisfying the given class
237      * <code>filter</code>. The search is performed on the static classpath (the one the application
238      * started with) and the dynamic classpath (the one specified using the load methods).
239      *
240      * @param <T> type of filtered class.
241      * @param type of filtered class.
242      * @return list of matching classes.
243      * @throws IOException if there is an error obtaining plugins.
244      */
245     public synchronized <T> Iterator<T> getPlugins(final Class<T> type)
246     throws IOException {
247         return ServiceLoader.load(type, dynamicClassLoader).iterator();
248     }
249 
250     /**
251      * Returns the list of all the {@link Tool} classes declared within the classpath.
252      *
253      * @return not <code>null</code> list of tool classes.
254      * @throws IOException if there is an error obtaining {@link org.apache.any23.cli.Tool}'s
255      * from the classpath. 
256      */
257     public synchronized Iterator<Tool> getTools() throws IOException {
258         return getPlugins(Tool.class);
259     }
260 
261     /**
262      * List of {@link ExtractorPlugin} classes declared within the classpath.
263      *
264      * @return not <code>null</code> list of plugin classes.
265      * @throws IOException if there is an error obtaining Extractors.
266      */
267     @SuppressWarnings("rawtypes")
268     public synchronized Iterator<ExtractorFactory> getExtractors() throws IOException {
269         return getPlugins(ExtractorFactory.class);
270     }
271 
272     /**
273      * Loads plugins from a list of specified locations.
274      *
275      * @param pluginLocations list of locations.
276      * @return a report about the loaded plugins.
277      */
278     public synchronized String loadPlugins(File... pluginLocations) {
279         final StringBuilder report = new StringBuilder();
280         report.append("\nLoading plugins from locations {\n");
281         for (File pluginLocation : pluginLocations) {
282             report.append(pluginLocation.getAbsolutePath()).append('\n');
283         }
284         report.append("}\n");
285 
286         final Throwable[] errors = loadFiles(pluginLocations);
287         if (errors.length > 0) {
288             report.append("The following errors occurred while loading plugins {\n");
289             for (Throwable error : errors) {
290                 report.append(error);
291                 report.append("\n\n\n");
292             }
293             report.append("}\n");
294         }
295         return report.toString();
296     }
297 
298     /**
299         * Configures a new list of extractors containing the extractors declared in <code>initialExtractorGroup</code>
300         * and also the extractors detected in classpath specified by <code>pluginLocations</code>.
301         *
302         * @param pluginLocations path locations of plugins.
303         * @return full list of extractors.
304         * @throws java.io.IOException if there is an error locating the plugin(s).
305         * @throws IllegalAccessException if there are access permissions for plugin(s).
306         * @throws InstantiationException if there is an error instantiating plugin(s).
307         */
308     public synchronized ExtractorGroup configureExtractors(
309             final File... pluginLocations
310     ) throws IOException, IllegalAccessException, InstantiationException {
311 
312         final String pluginsReport = loadPlugins(pluginLocations);
313         logger.info(pluginsReport);
314 
315         final StringBuilder report = new StringBuilder();
316         try {
317             final List<ExtractorFactory<?>> newFactoryList = new ArrayList<>();
318             @SuppressWarnings("rawtypes")
319             Iterator<ExtractorFactory> extractors = getExtractors();
320             while (extractors.hasNext()) {
321                 ExtractorFactory<?> factory = extractors.next();
322 
323                 report.append("\n - found plugin: ").append(factory.getExtractorName()).append("\n");
324 
325                 newFactoryList.add(factory);
326             }
327 
328             if (newFactoryList.isEmpty()) {
329                 report.append("\n=== No plugins have been found.===\n");
330             }
331 
332             return new ExtractorGroup(newFactoryList);
333         } finally {
334             logger.info(report.toString());
335         }
336     }
337 
338     /**
339      * Configures a new list of extractors containing the extractors declared in <code>initialExtractorGroup</code>
340      * and also the extractors detected in classpath specified by the default configuration.
341      *
342      * @param initialExtractorGroup initial list of extractors.
343      * @return full list of extractors.
344      * @throws java.io.IOException if there is an error locating the extractor(s).
345      * @throws IllegalAccessException if there are access permissions for extractor(s).
346      * @throws InstantiationException if there is an error instantiating extractor(s).
347      */
348     public synchronized ExtractorGroupache/any23/extractor/ExtractorGroup.html#ExtractorGroup">ExtractorGroup configureExtractors(ExtractorGroup initialExtractorGroup)
349     throws IOException, InstantiationException, IllegalAccessException {
350         final String pluginDirs = DefaultConfiguration.singleton().getPropertyOrFail(PLUGIN_DIRS_PROPERTY);
351         final File[] pluginLocations = getPluginLocations(pluginDirs);
352         return configureExtractors(pluginLocations);
353     }
354 
355     /**
356      * Returns an extractor group containing both the default extractors declared by the
357      * {@link org.apache.any23.extractor.ExtractorRegistry} and the {@link ExtractorPlugin}s.
358      * @param registry an {@link org.apache.any23.extractor.ExtractorRegistry}
359      * @param pluginLocations optional list of plugin locations.
360      *
361      * @return a not <code>null</code> and not empty extractor group.
362      * @throws java.io.IOException if there is an error locating the extractor group.
363      * @throws IllegalAccessException if there are access permissions for the extractor group.
364      * @throws InstantiationException if there is an error instantiating the extractor group.
365      */
366     public synchronized ExtractorGroup getApplicableExtractors(ExtractorRegistry registry, File... pluginLocations)
367     throws IOException, IllegalAccessException, InstantiationException {
368         return configureExtractors(pluginLocations);
369     }
370 
371     /**
372      * Returns an {@link Iterator} of tools that have been detected within the given list of locations.
373      *
374      * @param pluginLocations list of plugin locations.
375      * @return set of detected tools.
376      * @throws IOException if there is an error acessing {@link org.apache.any23.cli.Tool}'s.
377      */
378     public synchronized Iterator<Tool> getApplicableTools(File... pluginLocations) throws IOException {
379         final String report = loadPlugins(pluginLocations);
380         logger.info(report);
381         return getTools();
382     }
383 
384     /**
385      * Converts a column separated list of dirs in a list of files.
386      *
387      * @param pluginDirsList
388      * @return
389      */
390     private File[] getPluginLocations(String pluginDirsList) {
391         final String[] locationsStr = pluginDirsList.split(PLUGIN_DIRS_LIST_SEPARATOR);
392         final List<File> locations = new ArrayList<>();
393         for(String locationStr : locationsStr) {
394             final File location = new File(locationStr);
395             if( ! location.exists()) {
396                 throw new IllegalArgumentException(
397                         String.format("Plugin location '%s' cannot be found.", locationStr)
398                 );
399             }
400             locations.add(location);
401         }
402         return locations.toArray(new File[locations.size()]);
403     }
404 
405     /**
406      * Dynamic local file class loader.
407      */
408     private static final class DynamicClassLoader extends URLClassLoader {
409 
410         private final Set<String> addedURLs = new HashSet<>();
411 
412         private final List<File> jars;
413 
414         private final List<File> dirs;
415 
416         public DynamicClassLoader(URL[] urls) {
417             super(urls, Any23PluginManager.class.getClassLoader());
418             jars = new ArrayList<>();
419             dirs = new ArrayList<>();
420         }
421 
422         public DynamicClassLoader() {
423             this(new URL[0]);
424         }
425 
426         public boolean addClassDir(File classDir) {
427             final String urlPath = "file://" + classDir.getAbsolutePath() + "/";
428             try {
429                 if( addURL(urlPath) ) {
430                     dirs.add(classDir);
431                     return true;
432                 }
433                 return false;
434             } catch (MalformedURLException murle) {
435                 throw new RuntimeException("Invalid dir URL.", murle);
436             }
437         }
438 
439         public boolean addJAR(File jar) {
440             final String urlPath = "jar:file://" + jar.getAbsolutePath() + "!/";
441             try {
442                 if (addURL(urlPath)) {
443                     jars.add(jar);
444                     return true;
445                 }
446                 return false;
447             } catch (MalformedURLException murle) {
448                 throw new RuntimeException("Invalid JAR URL.", murle);
449             }
450         }
451 
452         private boolean addURL(String urlPath) throws MalformedURLException {
453             if(addedURLs.contains(urlPath)) {
454                 return false;
455             }
456             super.addURL(new URL(urlPath));
457             addedURLs.add(urlPath);
458             return true;
459         }
460     }
461 
462 }