Source code: com/dghda/module/ModuleLoader.java
1 /* Copyright (C) 2001 Duane Griffin <duanegriffin@users.sourceforge.net>
2 This file is part of Kent.
3
4 Kent is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License as
6 published by the Free Software Foundation; either version 2 of the
7 License, or (at your option) any later version.
8
9 Kent is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 General Public License for more details.
13
14 You should have received a copy of the GNU General Public
15 License along with Kent; see the file COPYING. If not,
16 write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17 Boston, MA 02111-1307, USA.
18 */
19
20 package com.dghda.module;
21
22 import java.io.*;
23 import java.util.*;
24 import java.util.jar.*;
25
26 import com.dghda.log.*;
27
28 /**
29 A module loader object manages a set of loadable objects.
30 It is given a path string, which can contain directories, files or jar files.
31 Directories and jars are scanned and their contents treated as files. These
32 are then treated as class files containing compiled module providers.
33 */
34 public class ModuleLoader {
35
36 /** Create a new module loader for the given class, using the given path(s), and no constructor arguments. */
37 public ModuleLoader (Class modClass, String modulePath, Logger logger) throws InvalidPathException {
38 this (modClass, parseModulePath (modulePath), null, logger);
39 }
40
41 /** Create a new module loader for the given class, using the given path(s), and the given constructor arguments. */
42 public ModuleLoader (Class modClass, String [] modulePaths, Logger logger) throws InvalidPathException {
43 this (modClass, modulePaths, null, logger);
44 }
45
46 /** Create a new module loader for the given class, using the given path(s) and constructor arguments. */
47 public ModuleLoader (Class modClass, String modulePath, Object [] args, Logger logger) throws InvalidPathException {
48 this (modClass, parseModulePath (modulePath), args, logger);
49 }
50
51 /** Create a new module loader for the given class, using the given path(s) and constructor arguments. */
52 public ModuleLoader (Class modClass, String [] modulePaths, Object [] args, Logger logger) throws InvalidPathException {
53
54 // Check the given class implements the Module interface
55 if (!Module.class.isAssignableFrom (modClass))
56 throw new ClassCastException (modClass.getName());
57
58 m_Logger = logger;
59 m_ModuleClass = modClass;
60 m_ConstructorArgs = args;
61
62 // Add components for every element in the path, logging any failures
63 for (int index = 0; index != modulePaths.length; ++index) {
64 try {
65 addModulePath (modulePaths[index]);
66 } catch (InvalidPathException exc) {
67 m_Logger.log ("Could not add path component " + modulePaths[index], exc);
68 }
69 }
70
71 // Load in all modules
72 scan (false);
73 }
74
75 /**
76 Add a module path component to the scan list.
77 @param path The path to add.
78 */
79 public void addModulePath (String path) throws InvalidPathException {
80 ModulePathComponent component = createPathComponent (path);
81 if (component != null)
82 m_ModulePaths.add (component);
83 }
84
85 /**
86 Remove a module path component from the scan list.
87 @param path The path to remove.
88 @returns False if the path was not in the scan list.
89 */
90 public boolean removeModulePath (String path) {
91 Iterator i = m_ModulePaths.iterator();
92 while (i.hasNext()) {
93 ModulePathComponent component = (ModulePathComponent) i.next();
94 if (component.getPath().equals (path)) {
95 i.remove();
96 return true;
97 }
98 }
99 return false;
100 }
101
102 /**
103 Attempt to add the given module.
104 The module will be added if it is not already present, or is with a lower version number.
105 This method can be used to add non-dynamic reports.
106 @returns True if the module was added.
107 @throws ClassCastException If the given module is not derived from the module loader's module class.
108 */
109 public boolean addModule (Module module) {
110
111 // Check the module is of the correct type
112 if (!m_ModuleClass.isAssignableFrom (module.getClass()))
113 throw new ClassCastException ("Module must be derived from " + m_ModuleClass.getName());
114
115 // Check whether this is new or a newer version, and if so replace it
116 Module current = (Module) m_Modules.get (module.getID());
117 if (current == null || module.getVersion().compareTo (current.getVersion()) > 0) {
118
119 // Log the fact we are loading the module
120 if (current == null) {
121 m_Logger.log ("Loading module " + module.getID() + " (" +
122 module.getID() + ") with version " +
123 module.getVersion());
124 } else {
125 m_Logger.log ("Replacing module " + module.getID() + " (" +
126 module.getID() + ") version " +
127 current.getVersion() + " with version " +
128 module.getVersion());
129 }
130
131 m_Modules.put (module.getID(), module);
132 return true;
133 } else {
134 return false;
135 }
136 }
137
138 /** Parses a path string in the same form as the classpath into an array of paths. */
139 public static String [] parseModulePath (String paths) {
140 StringTokenizer tokens = new StringTokenizer (paths, File.pathSeparator);
141 String [] result = new String[tokens.countTokens()];
142 int index = 0;
143 while (tokens.hasMoreTokens())
144 result[index++] = tokens.nextToken();
145 return result;
146 }
147
148 /** Returns a list of all modules available to the given user. */
149 public synchronized List getAvailableModules() {
150
151 // Update the list
152 scan (true);
153
154 // Construct & return the result
155 return new ArrayList (m_Modules.values());
156 }
157
158 /**
159 Returns the given module.
160 @param id The module's ID.
161 @returns null if no such module exists.
162 */
163 public synchronized Module getModule (String id) {
164 return (Module) m_Modules.get (id);
165 }
166
167 /**
168 Scans the module path(s) for any new or updated modules.
169 New modules will be added to the list of available modules.
170 If a module class file has a more recent timestamp than the version that was used to load the module, it will be re-loaded.
171 @param updated If true then only new module providers, or ones modified by the last scan will be checked.
172 */
173 public synchronized void scan (boolean updated) {
174
175 // Scan all paths
176 Iterator components = m_ModulePaths.iterator();
177 while (components.hasNext()) {
178 ModulePathComponent component = (ModulePathComponent) components.next();
179
180 // Iterate over the contents of the path
181 Iterator providers = component.getProviders (updated);
182 while (providers.hasNext()) {
183 ModulePathComponent.Provider provider = (ModulePathComponent.Provider) providers.next();
184 checkProvidersModules (provider);
185 }
186 }
187 }
188
189 /** Constructs a path string in the same form as the classpath from an array of paths. */
190 public static String constructModulePath (String [] paths) {
191 StringBuffer buffer = new StringBuffer();
192 for (int index = 0; index != paths.length; ++index) {
193
194 // Seperate paths
195 if (index != 0)
196 buffer.append (File.pathSeparator);
197
198 // Add next path
199 buffer.append (paths[index]);
200 }
201
202 return buffer.toString();
203 }
204
205 /** Converts a string into a ModulePathComponent object. */
206 protected ModulePathComponent createPathComponent (String path) throws InvalidPathException {
207 ModulePathComponent result;
208
209 if (path.endsWith (".class")) {
210
211 // If the component is a class file...
212 result = new ModulePathFileComponent (path);
213 } else if (path.endsWith (".jar")) {
214
215 // If the component is a jar file...
216 result = new ModulePathJarComponent (path);
217 } else {
218 try {
219
220 // Check whether the path is a valid class
221 // This is a bit of a hack
222 Class providerClass = Class.forName (path);
223 if (checkProvidersModules (providerClass))
224 return null;
225 } catch (ClassNotFoundException exc) {
226 }
227
228 // Otherwise assume the component is a directory
229 result = new ModulePathDirComponent (path);
230 }
231
232 return result;
233 }
234
235 /**
236 (Re)loads the modules provided by the given provider, if appropriate.
237 Reports are loaded if they don't already exist, or if there is a newer version.
238 By using new class loaders each time we ensure that we don't use old versions of the module.
239 */
240 protected synchronized boolean checkProvidersModules (ModulePathComponent.Provider component) {
241
242 // First load a new version of the provider's class and construct an instance of it
243 try {
244 StreamClassLoader loader = new StreamClassLoader (getClass().getClassLoader());
245 Class providerClass = loader.loadClass (null, component.getInputStream());
246 return checkProvidersModules (providerClass);
247 } catch (Exception exc) {
248 m_Logger.log ("An error occurred while attempting to check module provider " + component.getName(), exc);
249 }
250
251 return false;
252 }
253
254 /**
255 (Re)loads the modules provided by the given provider, if appropriate.
256 Reports are loaded if they don't already exist, or if there is a newer version.
257 By using new class loaders each time we ensure that we don't use old versions of the module.
258 */
259 protected synchronized boolean checkProvidersModules (Class providerClass) {
260 boolean result = false;
261
262 try {
263 ModuleProvider provider = null;
264 if (m_ConstructorArgs == null) {
265
266 // Use the constructor with no arguments
267 provider = (ModuleProvider) providerClass.newInstance();
268 } else {
269
270 // Try all constructors using the given arguments
271 Exception lastException = null;
272 java.lang.reflect.Constructor [] constructors = providerClass.getConstructors();
273 for (int index = 0; index != constructors.length; ++index) {
274
275 // TODO: Check whether the constructor takes appropriate arguments, instead of letting it throw
276 try {
277
278 // Try and use the constructor to create a new object
279 provider = (ModuleProvider) constructors[index].newInstance (m_ConstructorArgs);
280
281 // If we succeed, stop
282 break;
283 } catch (Exception exc) {
284
285 // If not, save the error for now
286 lastException = exc;
287 }
288 }
289
290 // If we couldn't create a module, we should throw
291 if (provider == null) {
292 if (lastException == null) {
293
294 // It looks like there were no public constructors
295 throw new Exception ("No accessible module constructor found");
296 } else {
297
298 // There was at least one, but all failed
299 throw lastException;
300 }
301 }
302 }
303
304 m_Logger.log ("Checking provider " + provider.getClass().getName());
305
306 // Iterate over all the modules provided
307 Iterator modules = provider.getModuleIterator();
308 while (modules.hasNext()) {
309 Module module = (Module) modules.next();
310 try {
311
312 // Try to add the module
313 addModule (module);
314 } catch (ClassCastException exc) {
315
316 // Log incorrect module types
317 // It should only appear once, unless the file is updated
318 m_Logger.log ("Unable to add module " + module.getID() + " as it is not derived from " + m_ModuleClass.getName());
319 }
320 }
321
322 // Indicate we successfully checked the provider
323 result = true;
324 } catch (Exception exc) {
325 m_Logger.log ("An error occurred while attempting to check module provider class " + providerClass.getName(), exc);
326 }
327
328 return result;
329 }
330
331 private LinkedList m_ModulePaths = new LinkedList();
332 private TreeMap m_Modules = new TreeMap();
333 private Logger m_Logger;
334 private Class m_ModuleClass;
335 private Object [] m_ConstructorArgs;
336 }