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       * Any23 Plugins package.
56       */
57      public static final String PLUGINS_PACKAGE = ExtractorPlugin.class.getPackage().getName();
58  
59      /**
60       * Property where look for plugins.
61       */
62      public static final String PLUGIN_DIRS_PROPERTY = "any23.plugin.dirs";
63  
64      /**
65       * List separator for the string declaring the plugin list.
66       */
67      public static final String PLUGIN_DIRS_LIST_SEPARATOR = ":";
68  
69      /**
70       * Internal logger.
71       */
72      private static final Logger logger = LoggerFactory.getLogger(Any23PluginManager.class);
73  
74      /**
75       * Singleton lazy instance.
76       */
77      private static final Any23PluginManager instance = new Any23PluginManager();
78  
79      /**
80       * Internal class loader used to dynamically load classes.
81       */
82      private final DynamicClassLoader dynamicClassLoader;
83  
84      /**
85       * @return a singleton instance of {@link Any23PluginManager}.
86       */
87      public static synchronized Any23PluginManager getInstance() {
88          return instance;
89      }
90  
91      /**
92       * Constructor.
93       */
94      private Any23PluginManager() {
95          dynamicClassLoader = new DynamicClassLoader();
96      }
97  
98      /**
99       * Loads a <i>JAR</i> file in the classpath.
100      *
101      * @param jar the JAR file to be loaded.
102      * @return <code>true</code> if the JAR is added for the first time to the classpath,
103      *         <code>false</code> otherwise.
104      * @throws MalformedURLException
105      */
106     public synchronized boolean loadJAR(File jar) {
107         if(jar == null) throw new NullPointerException("jar file cannot be null.");
108         if (!jar.isFile() && !jar.exists()) {
109             throw new IllegalArgumentException(
110                     String.format("Invalid JAR [%s], must be an existing file.", jar.getAbsolutePath())
111             );
112         }
113         return dynamicClassLoader.addJAR(jar);
114     }
115 
116     /**
117      * Loads a list of <i>JAR</i>s in the classpath.
118      *
119      * @param jars list of JARs to be loaded.
120      * @return list of exceptions raised during the loading.
121      */
122     public synchronized Throwable[] loadJARs(File... jars) {
123         final List<Throwable> result = new ArrayList<Throwable>();
124         for (File jar : jars) {
125             try {
126                 loadJAR(jar);
127             } catch (Throwable t) {
128                 result.add(
129                         new IllegalArgumentException(
130                                 String.format("Error while loading jar [%s]", jar.getAbsolutePath()),
131                                 t
132                         )
133                 );
134             }
135         }
136         return result.toArray(new Throwable[result.size()]);
137     }
138 
139     /**
140      * Loads a <i>classes</i> directory in the classpath.
141      *
142      * @param classDir the directory to be loaded.
143      * @return <code>true</code> if the directory is added for the first time to the classpath,
144      *         <code>false</code> otherwise.
145      */
146     public synchronized boolean loadClassDir(File classDir) {
147         if(classDir == null) throw new NullPointerException("classDir cannot be null.");
148         if (!classDir.isDirectory() && !classDir.exists()) {
149             throw new IllegalArgumentException(
150                     String.format("Invalid class dir [%s], must be an existing file.", classDir.getAbsolutePath())
151             );
152         }
153         return dynamicClassLoader.addClassDir(classDir);
154     }
155 
156     /**
157      * Loads a list of class dirs in the classpath.
158      *
159      * @param classDirs list of class dirs to be loaded.
160      * @return  list of exceptions raised during the loading.
161      */
162     public synchronized Throwable[] loadClassDirs(File... classDirs) {
163         final List<Throwable> result = new ArrayList<Throwable>();
164         for (File classDir : classDirs) {
165             try {
166                 loadClassDir(classDir);
167             } catch (Throwable t) {
168                 result.add(
169                         new IllegalArgumentException(
170                                 String.format("Error while loading class dir [%s]", classDir.getAbsolutePath()),
171                                 t
172                         )
173                 );
174             }
175         }
176         return result.toArray(new Throwable[result.size()]);
177     }
178 
179     /**
180      * Loads all the JARs detected in a given directory.
181      *
182      * @param jarDir directory containing the JARs to be loaded.
183      * @return <code>true</code> if all JARs in dir are loaded.
184      */
185     public synchronized boolean loadJARDir(File jarDir) {
186         if(jarDir == null)
187             throw new NullPointerException("JAR dir must be not null.");
188         if(  ! jarDir.exists() )
189             throw new IllegalArgumentException("Given directory doesn't exist:" + jarDir.getAbsolutePath());
190         if(! jarDir.isDirectory() )
191             throw new IllegalArgumentException(
192                     "given file exists and it is not a directory: " + jarDir.getAbsolutePath()
193             );
194         boolean loaded = true;
195         for (File jarFile : jarDir.listFiles(
196                 new FilenameFilter() {
197                     @Override
198                     public boolean accept(File dir, String name) {
199                         return name.endsWith(".jar");
200                     }
201                 })
202         ) {
203             loaded &= loadJAR(jarFile);
204         }
205         return loaded;
206     }
207 
208     /**
209      * Loads a generic list of files, trying to determine the type of every file.
210      *
211      * @param files list of files to be loaded.
212      * @return list of errors occurred during loading.
213      */
214     public synchronized Throwable[] loadFiles(File... files) {
215         final List<Throwable> errors = new ArrayList<Throwable>();
216         for(File file : files) {
217             try {
218                 if (file.isFile() && file.getName().endsWith(".jar")) {
219                     loadJAR(file);
220                 } else if (file.isDirectory()) {
221                     if (file.getName().endsWith("classes")) {
222                         loadClassDir(file);
223                     } else {
224                         loadJARDir(file);
225                     }
226                 } else {
227                     throw new IllegalArgumentException("Cannot handle file " + file.getAbsolutePath());
228                 }
229             } catch (Throwable t) {
230                 errors.add(t);
231             }
232         }
233         return errors.toArray(new Throwable[errors.size()]);
234     }
235 
236     /**
237      * Returns all classes within the specified <code>packageName</code> satisfying the given class
238      * <code>filter</code>. The search is performed on the static classpath (the one the application
239      * started with) and the dynamic classpath (the one specified using the load methods).
240      *
241      * @param <T> type of filtered class.
242      * @return list of matching classes.
243      * @throws IOException
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
255      */
256     public synchronized Iterator<Tool> getTools() throws IOException {
257         return getPlugins(Tool.class);
258     }
259 
260     /**
261      * List of {@link ExtractorPlugin} classes declared within the classpath.
262      *
263      * @return not <code>null</code> list of plugin classes.
264      * @throws IOException
265      */
266     public synchronized Iterator<ExtractorPlugin> getExtractors() throws IOException {
267         return getPlugins(ExtractorPlugin.class);
268     }
269 
270     /**
271      * Loads plugins from a list of specified locations.
272      *
273      * @param pluginLocations list of locations.
274      * @return a report about the loaded plugins.
275      */
276     public synchronized String loadPlugins(File... pluginLocations) {
277         final StringBuilder report = new StringBuilder();
278         report.append("\nLoading plugins from locations {\n");
279         for (File pluginLocation : pluginLocations) {
280             report.append(pluginLocation.getAbsolutePath()).append('\n');
281         }
282         report.append("}\n");
283 
284         final Throwable[] errors = loadFiles(pluginLocations);
285         if (errors.length > 0) {
286             report.append("The following errors occurred while loading plugins {\n");
287             for (Throwable error : errors) {
288                 report.append(error);
289                 report.append("\n\n\n");
290             }
291             report.append("}\n");
292         }
293         return report.toString();
294     }
295 
296     /**
297         * Configures a new list of extractors containing the extractors declared in <code>initialExtractorGroup</code>
298         * and also the extractors detected in classpath specified by <code>pluginLocations</code>.
299         *
300         * @param initialExtractorGroup initial list of extractors.
301         * @param pluginLocations
302         * @return full list of extractors.
303         * @throws java.io.IOException
304         * @throws IllegalAccessException
305         * @throws InstantiationException
306         */
307     public synchronized ExtractorGroup configureExtractors(
308             final ExtractorGroup initialExtractorGroup,
309             final File... pluginLocations
310     ) throws IOException, IllegalAccessException, InstantiationException {
311         if (initialExtractorGroup == null) throw new NullPointerException("inExtractorGroup cannot be null");
312 
313         final String pluginsReport = loadPlugins(pluginLocations);
314         logger.info(pluginsReport);
315 
316         final StringBuilder report = new StringBuilder();
317         try {
318             final List<ExtractorFactory<?>> newFactoryList = new ArrayList<ExtractorFactory<?>>();
319             Iterator<ExtractorPlugin> extractors = getExtractors();
320             while (extractors.hasNext()) {
321                 ExtractorFactory<?> factory = extractors.next().getExtractorFactory();
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             for (ExtractorFactory<?> extractorFactory : initialExtractorGroup) {
333                 newFactoryList.add(extractorFactory);
334             }
335 
336             return new ExtractorGroup(newFactoryList);
337         } finally {
338             logger.info(report.toString());
339         }
340     }
341 
342     /**
343      * Configures a new list of extractors containing the extractors declared in <code>initialExtractorGroup</code>
344      * and also the extractors detected in classpath specified by the default configuration.
345      *
346      * @param initialExtractorGroup initial list of extractors.
347      * @return full list of extractors.
348      * @throws IOException
349      * @throws InstantiationException
350      * @throws IllegalAccessException
351      */
352     public synchronized ExtractorGroup configureExtractors(ExtractorGroup initialExtractorGroup)
353     throws IOException, InstantiationException, IllegalAccessException {
354         final String pluginDirs = DefaultConfiguration.singleton().getPropertyOrFail(PLUGIN_DIRS_PROPERTY);
355         final File[] pluginLocations = getPluginLocations(pluginDirs);
356         return configureExtractors(initialExtractorGroup, pluginLocations);
357     }
358 
359     /**
360      * Returns an extractor group containing both the default extractors declared by the
361      * {@link org.apache.any23.extractor.ExtractorRegistry} and the {@link ExtractorPlugin}s.
362      * @param registry TODO
363      * @param pluginLocations optional list of plugin locations.
364      *
365      * @return a not <code>null</code> and not empty extractor group.
366      * @throws java.io.IOException
367      * @throws IllegalAccessException
368      * @throws InstantiationException
369      */
370     public synchronized ExtractorGroup getApplicableExtractors(ExtractorRegistry registry, File... pluginLocations)
371     throws IOException, IllegalAccessException, InstantiationException {
372         final ExtractorGroup defaultExtractors = registry.getExtractorGroup();
373         return configureExtractors(defaultExtractors, pluginLocations);
374     }
375 
376     /**
377      * Returns an {@link Iterator} of tools that have been detected within the given list of locations.
378      *
379      * @param pluginLocations list of plugin locations.
380      * @return set of detected tools.
381      * @throws IOException
382      */
383     public synchronized Iterator<Tool> getApplicableTools(File... pluginLocations) throws IOException {
384         final String report = loadPlugins(pluginLocations);
385         logger.info(report);
386         return getTools();
387     }
388 
389     /**
390      * Converts a column separated list of dirs in a list of files.
391      *
392      * @param pluginDirsList
393      * @return
394      */
395     private File[] getPluginLocations(String pluginDirsList) {
396         final String[] locationsStr = pluginDirsList.split(PLUGIN_DIRS_LIST_SEPARATOR);
397         final List<File> locations = new ArrayList<File>();
398         for(String locationStr : locationsStr) {
399             final File location = new File(locationStr);
400             if( ! location.exists()) {
401                 throw new IllegalArgumentException(
402                         String.format("Plugin location '%s' cannot be found.", locationStr)
403                 );
404             }
405             locations.add(location);
406         }
407         return locations.toArray(new File[locations.size()]);
408     }
409 
410     /**
411      * Dynamic local file class loader.
412      */
413     private static final class DynamicClassLoader extends URLClassLoader {
414 
415         private final Set<String> addedURLs = new HashSet<String>();
416 
417         private final List<File> jars;
418 
419         private final List<File> dirs;
420 
421         public DynamicClassLoader(URL[] urls) {
422             super(urls, Any23PluginManager.class.getClassLoader());
423             jars = new ArrayList<File>();
424             dirs = new ArrayList<File>();
425         }
426 
427         public DynamicClassLoader() {
428             this(new URL[0]);
429         }
430 
431         public boolean addClassDir(File classDir) {
432             final String urlPath = "file://" + classDir.getAbsolutePath() + "/";
433             try {
434                 if( addURL(urlPath) ) {
435                     dirs.add(classDir);
436                     return true;
437                 }
438                 return false;
439             } catch (MalformedURLException murle) {
440                 throw new RuntimeException("Invalid dir URL.", murle);
441             }
442         }
443 
444         public boolean addJAR(File jar) {
445             final String urlPath = "jar:file://" + jar.getAbsolutePath() + "!/";
446             try {
447                 if (addURL(urlPath)) {
448                     jars.add(jar);
449                     return true;
450                 }
451                 return false;
452             } catch (MalformedURLException murle) {
453                 throw new RuntimeException("Invalid JAR URL.", murle);
454             }
455         }
456 
457         private boolean addURL(String urlPath) throws MalformedURLException {
458             if(addedURLs.contains(urlPath)) {
459                 return false;
460             }
461             super.addURL(new URL(urlPath));
462             addedURLs.add(urlPath);
463             return true;
464         }
465     }
466 
467 }