source: trunk/src/org/expeditee/actions/Actions.java@ 80

Last change on this file since 80 was 80, checked in by ra33, 16 years ago

Added some more unit tests
Did a bunch of refactoring
AND added a few new features... @b @v were the most significant

File size: 17.8 KB
Line 
1package org.expeditee.actions;
2
3import java.io.File;
4import java.lang.reflect.Constructor;
5import java.lang.reflect.Method;
6import java.lang.reflect.Modifier;
7import java.net.URL;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Enumeration;
11import java.util.HashMap;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.jar.JarFile;
15import java.util.zip.ZipEntry;
16
17import org.expeditee.agents.Agent;
18import org.expeditee.gui.DisplayIO;
19import org.expeditee.gui.Frame;
20import org.expeditee.gui.FrameGraphics;
21import org.expeditee.gui.FrameIO;
22import org.expeditee.gui.FrameUtils;
23import org.expeditee.io.Conversion;
24import org.expeditee.io.Logger;
25import org.expeditee.items.Item;
26import org.expeditee.items.ItemUtils;
27
28/**
29 * The Action class is used to launch Actions and Agents.
30 *
31 * This class checks all class files in the same directory, and reads in and
32 * adds all the methods from them. The methods are stored in a Hashtable so that
33 * the lowercase method names can be mapped to the correctly capatilized method
34 * names (to provide case-insensitivity)
35 *
36 * When adding an action to a class in the actions folder the following must be
37 * considered:
38 * <li> If the first parameter is of type Frame or Item they will be called with
39 * the current frame and the item used to execute the action.</li>
40 * <li> If the second item is also of type Frame or Item it will be set to the
41 * current frame or item.</li>
42 * <li> If there are multiple overloads for the same method they should be
43 * declared in order of the methods with the most parameteres to least
44 * parameters.</li>
45 */
46public class Actions {
47
48 // the currently running agent (if there is one)
49 private static Agent _Agent = null;
50
51 // maps lower case method names to the method
52 private static HashMap<String, Method> _Actions = new HashMap<String, Method>();
53
54 // maps lower case JAG class names to capitalized JAG names
55 private static HashMap<String, String> _JAGs = new HashMap<String, String>();
56
57 public static final String ROOT_PACKAGE = "org.expeditee.";
58
59 // Package and class file locations
60 private static final String ACTIONS_PACKAGE = ROOT_PACKAGE + "actions.";
61
62 private static final String AGENTS_PACKAGE = ROOT_PACKAGE + "agents.";
63
64 private static final String NAVIGATIONS_CLASS = ROOT_PACKAGE
65 + "actions.NavigationActions";
66
67 public static Class[] getClasses(String pckgname)
68 throws ClassNotFoundException {
69 ArrayList<Class> classes = new ArrayList<Class>();
70 // Get a File object for the package
71 File directory = null;
72 String path = pckgname.replace('.', File.separatorChar);
73 try {
74 ClassLoader cld = Thread.currentThread().getContextClassLoader();
75 if (cld == null) {
76 throw new ClassNotFoundException("Can't get class loader.");
77 }
78 URL resource = null;
79 try {
80 Enumeration<URL> resources = cld.getResources(path);
81 while (resources.hasMoreElements()) {
82 URL url = resources.nextElement();
83 // Ingore the classes in the test folder when we are running
84 // the program from Eclipse
85 // This doesnt apply when running directly from the jar
86 // because the test classes are not compiled into the jar.
87 if (!url.toString().toLowerCase().contains("test")) {
88 resource = url;
89 break;
90 }
91 }
92 } catch (Exception e) {
93
94 }
95 if (resource == null) {
96 throw new ClassNotFoundException("No resource for " + path);
97 }
98 directory = new File(resource.getFile());
99 } catch (NullPointerException x) {
100 throw new ClassNotFoundException(pckgname + " (" + directory
101 + ") does not appear to be a valid package");
102 }
103
104 int splitPoint = directory.getPath().indexOf('!');
105 if (splitPoint > 0) {
106 try {
107 String jarName = directory.getPath().substring(
108 "file:".length(), splitPoint);
109 // Windows HACK
110 if (jarName.indexOf(":") >= 0)
111 jarName = jarName.substring(1);
112
113 if (jarName.indexOf("%20") > 0) {
114 jarName = jarName.replace("%20", " ");
115 }
116
117 JarFile jarFile = new JarFile(jarName);
118
119 Enumeration entries = jarFile.entries();
120 while (entries.hasMoreElements()) {
121 ZipEntry entry = (ZipEntry) entries.nextElement();
122 String className = entry.getName();
123 if (className.startsWith(path)) {
124 if (className.endsWith(".class")
125 && !className.contains("$")) {
126 classes.add(Class.forName(className.substring(0,
127 className.length() - 6).replace('/', '.')));
128 }
129 }
130 }
131 jarFile.close();
132
133 } catch (Exception e) {
134 e.printStackTrace();
135 }
136
137 } else {
138
139 if (directory.exists()) {
140 // Get the list of the files contained in the package
141 String[] files = directory.list();
142 for (int i = 0; i < files.length; i++) {
143 // we are only interested in .class files
144 if (files[i].endsWith(".class") && !files[i].contains("$")
145 && !files[i].equals("Actions.class")) {
146 // removes the .class extension
147 classes
148 .add(Class.forName(pckgname
149 + files[i].substring(0, files[i]
150 .length() - 6)));
151 }
152 }
153 } else {
154 throw new ClassNotFoundException(pckgname + " (" + directory
155 + ") does not appear to be a valid package");
156 }
157 }
158 Class[] classesA = new Class[classes.size()];
159 classes.toArray(classesA);
160 return classesA;
161 }
162
163 /**
164 * Clears out the Action and JAG Hashtables and refills them. Normally this
165 * is only called once when the system starts.
166 * @return a warning message if there were any problems loading agents or actions.
167 */
168 public static Collection<String> Init() {
169 Collection<String> warnings = new LinkedList<String>();
170 Class[] classes;
171
172 try {
173 classes = getClasses(AGENTS_PACKAGE);
174
175 for (int i = 0; i < classes.length; i++) {
176 String name = classes[i].getSimpleName();
177 // maps lower case name to correct capitalised name
178 _JAGs.put(name.toLowerCase(), name);
179 }
180 } catch (Exception e) {
181 warnings.add("You must have Java 1.5 or higher to run Expeditee");
182 warnings.add(e.getMessage());
183 }
184 try {
185 classes = getClasses(ACTIONS_PACKAGE);
186
187 for (int i = 0; i < classes.length; i++) {
188 String name = classes[i].getSimpleName();
189 // Ignore the test classes
190 if (name.toLowerCase().contains("test"))
191 continue;
192 // read in all the methods from the class
193 try {
194 // System.out.println(name)
195 LoadMethods(Class.forName(ACTIONS_PACKAGE + name));
196 } catch (ClassNotFoundException e) {
197 Logger.Log(e);
198 e.printStackTrace();
199 }
200 }
201 } catch (Exception e) {
202 warnings.add(e.getMessage());
203 }
204 return warnings;
205 }
206
207 /**
208 * Loads all the Methods that meet the requirements checked by MethodCheck
209 * into the hashtable.
210 *
211 * @param c
212 * The Class to load the Methods from.
213 */
214 private static void LoadMethods(Class c) {
215 // list of methods to test
216 Method[] toLoad = c.getMethods();
217
218 for (Method m : toLoad) {
219 // only allow methods with the right modifiers
220 if (MethodCheck(m)) {
221 String lowercaseName = m.getName().toLowerCase();
222 if (!(_Actions.containsKey(lowercaseName)))
223 _Actions.put(lowercaseName, m);
224 else {
225 int i = 0;
226 while (_Actions.containsKey(lowercaseName + i))
227 i++;
228
229 _Actions.put(lowercaseName + i, m);
230 }
231
232 }
233 }
234 }
235
236 /**
237 * Checks if the given Method corresponds to the restrictions of Action
238 * commands, namely: Declared (not inherited), Public, and Static, with a
239 * void return type.
240 *
241 * @param m
242 * The Method to check
243 * @return True if the Method meets the above conditions, false otherwise.
244 */
245 private static boolean MethodCheck(Method m) {
246 // the return type must be void
247 if (m.getReturnType() != void.class)
248 return false;
249
250 int mods = m.getModifiers();
251
252 // check the method is declared (not inherited)
253 if ((mods & Method.DECLARED) != Method.DECLARED)
254 return false;
255
256 // check the method is public
257 if ((mods & Modifier.PUBLIC) != Modifier.PUBLIC)
258 return false;
259
260 // check the method is static
261 if ((mods & Modifier.STATIC) != Modifier.STATIC)
262 return false;
263
264 // if we have not returned yet, then the tests have all passed
265 return true;
266 }
267
268 /**
269 * Performs the given action command. The source Frame and Item are given
270 * because they are required by some actions. Note that the source frame
271 * does not have to be the Item's parent Frame.
272 *
273 * @param source
274 * The Frame that the action should apply to
275 * @param launcher
276 * The Item that has the action assigned to it
277 * @param command
278 * The action to perform
279 */
280 public static void PerformAction(Frame source, Item launcher, String command) {
281 if (!command.equalsIgnoreCase("Restore"))
282 FrameIO.SaveFrame(source, false);
283
284 // separate method name and parameter names
285 String mname = getName(command);
286 if (command.length() > mname.length())
287 command = command.substring(mname.length() + 1);
288 else
289 command = "";
290
291 // Strip off the @ from annotation items
292 if (mname.startsWith("@"))
293 mname = mname.substring(1);
294
295 mname = mname.trim();
296 String lowercaseName = mname.toLowerCase();
297 // check for protection on frame
298 if (ItemUtils.ContainsTag(source.getItems(), "@No" + mname)) {
299 FrameGraphics.DisplayMessage("Frame is protected by @No" + mname
300 + " tag.");
301 return;
302 }
303
304 // retrieve methods that match the name
305 Method toRun = _Actions.get(lowercaseName);
306
307 // if this is not the name of a method, it may be the name of an agent
308 if (toRun == null) {
309 LaunchAgent(mname, command, source);
310 return;
311 }
312
313 // Need to save the frame if we are navigating away from it so we dont
314 // loose changes
315 if (toRun.getDeclaringClass().getName().equals(NAVIGATIONS_CLASS)) {
316 FrameIO.SaveFrame(DisplayIO.getCurrentFrame());
317 }
318
319 // if there are duplicate methods with the same name
320 List<Method> possibles = new LinkedList<Method>();
321 possibles.add(toRun);
322 int i = 0;
323 while (_Actions.containsKey(lowercaseName + i)) {
324 possibles.add(_Actions.get(lowercaseName + i));
325 i++;
326 }
327
328 for (Method possible : possibles) {
329
330 // try first with the launching item as a parameter
331
332 // run method
333 try {
334 // convert parameters to objects and get the method to invoke
335 Object[] parameters = CreateObjects(possible, source, launcher,
336 command);
337 // Check that there are the same amount of params
338 if (parameters == null) {
339 continue;
340 }
341
342 possible.invoke(null, parameters);
343 return;
344 } catch (Exception e) {
345 Logger.Log(e);
346 e.printStackTrace();
347 }
348 }
349 }
350
351 /**
352 * Launches an agent with the given name, and passes in the given parameters
353 *
354 * @param name
355 * The name of the JAG to load
356 * @param parameters
357 * The parameters to pass to the JAG
358 * @param source
359 * The starting Frame that the JAG is being launched on
360 */
361 private static void LaunchAgent(String name, String parameters, Frame source) {
362 // Use the correct case version for printing error messages
363 String nameWithCorrectCase = name;
364 name = name.toLowerCase();
365 // save the current frame (if necesssary)
366 FrameUtils.LeavingFrame(source);
367
368 try {
369 // check for stored capitalisation
370 if (_JAGs.containsKey(name.toLowerCase())) {
371 name = _JAGs.get(name.toLowerCase());
372 } else if (name.toLowerCase().endsWith("tree")) {
373 parameters = name.substring(0, name.length() - "tree".length())
374 + " " + parameters;
375 name = "WriteTree";
376 } else if (name.toLowerCase().endsWith("frame")) {
377 parameters = name
378 .substring(0, name.length() - "frame".length())
379 + " " + parameters;
380 name = "WriteFrame";
381 }
382
383 // load the JAG class
384 Class agentClass = Class.forName(AGENTS_PACKAGE + name);
385
386 // get the constructor for the JAG class
387 Constructor con = null;
388 Constructor constructors[] = agentClass.getConstructors();
389 Object[] params = null;
390
391 // determine correct parameters for constructor
392 for (Constructor c : constructors) {
393 if (parameters.length() > 0
394 && c.getParameterTypes().length == 1) {
395 con = c;
396 params = new String[1];
397 params[0] = parameters;
398 break;
399 } else if (c.getParameterTypes().length == 0 && con == null)
400 con = c;
401 }
402
403 // if there is no constructor, return
404 if (con == null) {
405 FrameGraphics.DisplayMessage("Invalid parametres for agent: "
406 + nameWithCorrectCase);
407 // System.out.println("Constructor not found...");
408 return;
409 }
410
411 // create the JAG
412 _Agent = (Agent) con.newInstance(params);
413
414 Thread t = new Thread(_Agent);
415 t.setPriority(Thread.MIN_PRIORITY);
416
417 // check for errors during initialisation
418 if (!_Agent.initialise(source)) {
419 FrameGraphics.ErrorMessage("Error initialising agent: "
420 + nameWithCorrectCase);
421 return;
422 }
423
424 _Agent.setStartFrame(source);
425
426 t.start();
427
428 // if there is a completion frame, then display it to the user
429 if (_Agent.hasResultFrame()) {
430 Frame next = _Agent.getResultFrame();
431 FrameUtils.DisplayFrame(next, true);
432 }
433
434 } catch (ClassNotFoundException cnf) {
435 FrameGraphics.ErrorMessage("Error: '" + nameWithCorrectCase
436 + "' is not an action or agent.");
437 } catch (Exception e) {
438 FrameGraphics.ErrorMessage("Error creating Agent: '"
439 + nameWithCorrectCase + "'");
440 System.out.println("Agent set to Null.");
441 _Agent = null;
442 e.printStackTrace();
443 Logger.Log(e);
444 }
445
446 return;
447 }
448
449 /**
450 * Used to determine if the previously launched agent is still executing.
451 *
452 * @return True if the last Agent is still executing, False otherwise.
453 */
454 public static boolean isAgentRunning() {
455 if (_Agent != null)
456 return _Agent.isRunning();
457
458 return false;
459 }
460
461 /**
462 * Stops the currently running Agent (If there is one) by calling
463 * Agent.stop(). Note: This may not stop the Agent immediately, but the
464 * Agent should terminate as soon as it is safe to do so.
465 */
466 public static void stopAgent() {
467 if (_Agent != null && _Agent.isRunning()) {
468 FrameGraphics.DisplayMessage("Stopping Agent...");
469 _Agent.stop();
470 }
471 }
472
473 public static void interruptAgent() {
474 if (_Agent != null) {
475 _Agent.interrupt();
476 }
477 }
478
479 /**
480 * Converts the given String of values into an array of Objects
481 *
482 * @param launcher
483 * The Item used to launch the action, it may be required as a
484 * parameter
485 * @param values
486 * A list of space separated String values to convert to objects
487 * @return The created array of Objects
488 */
489 public static Object[] CreateObjects(Method method, Frame source,
490 Item launcher, String values) {
491 // The parameter types that should be created from the given String
492 Class[] paramTypes = method.getParameterTypes();
493
494 // if the method has no parameters
495 if (paramTypes.length == 0)
496 return new Object[0];
497
498 Object[] objects = new Object[paramTypes.length];
499 int ind = 0;
500
501 // if the first class in the list is a frame or item, it is the source
502 // or launcher
503 // length must be at least one if we are still running
504 if (paramTypes[0] == Frame.class) {
505 objects[0] = source;
506 ind = 1;
507 } else if (paramTypes[0] == Item.class) {
508 objects[0] = launcher;
509 ind = 1;
510 }
511
512 // if the first class was a frame or item, the second class might be the
513 // other
514 if (paramTypes.length > 1 && objects[0] != null) {
515 if (paramTypes[1] == Frame.class) {
516 objects[1] = source;
517 ind = 2;
518 } else if (paramTypes[1] == Item.class) {
519 objects[1] = launcher;
520 ind = 2;
521 }
522 }
523
524 String param = values;
525 // convert the rest of the objects
526 for (; ind < objects.length; ind++) {
527 // check if its the last param and combine
528 if (values.length() > 0 && ind == objects.length - 1) {
529 param = values.trim();
530 // check if its a string
531 if (param.length() > 0 && param.charAt(0) == '"') {
532 int endOfString = param.indexOf('"', 1);
533 if (endOfString > 0) {
534 param = param.substring(0, endOfString);
535 }
536 }
537 } else {// strip off the next value
538 param = ParseValue(values);
539 values = RemainingParams(values);
540 }
541 // convert the value to an object
542 Object o = Conversion.Convert(paramTypes[ind], param);
543 if (o == null)
544 return null;
545 objects[ind] = o;
546 }
547
548 return objects;
549 }
550
551 /**
552 * Returns a string containing the remaining params after ignoring the first
553 * one.
554 *
555 * @param params
556 * a space sparated list of N parameters
557 * @return the remaining N - 1 parameters
558 */
559 public static String RemainingParams(String params) {
560 if (params.length() == 0)
561 return null;
562
563 // remove leading and trailing spaces
564 params = params.trim();
565
566 // if there are no more parameters, we are done
567 if (params.indexOf(" ") < 0) {
568 return "";
569 }
570
571 // Check if we have a string parameter
572 if (params.charAt(0) == '"') {
573 int endOfString = params.indexOf('"', 1);
574 if (endOfString > 0) {
575 if (endOfString > params.length())
576 return "";
577 return params.substring(endOfString + 1).trim();
578 }
579 }
580
581 return params.substring(params.indexOf(" ")).trim();
582 }
583
584 /**
585 * Returns the first value in the space separated String of parameters
586 * passed in. Strings are enclosed in double quotes.
587 *
588 * @param params
589 * The String of space separated values
590 * @return The first value in the String
591 */
592 public static String ParseValue(String params) {
593 if (params.length() == 0)
594 return null;
595
596 // remove leading and trailing spaces
597 String param = params.trim();
598
599 // Check if we have a string parameter
600 if (param.charAt(0) == '"') {
601 int endOfString = param.indexOf('"', 1);
602 if (endOfString > 0)
603 return param.substring(1, endOfString);
604 }
605
606 // if there are no more parameters, we are done
607 if (param.indexOf(" ") < 0) {
608 return param;
609 }
610
611 return param.substring(0, param.indexOf(" "));
612 }
613
614 /**
615 * Separates the name of the given command from any parameters and returns
616 * them
617 *
618 * @param command
619 * The String to separate out the Action or Agent name from
620 * @return The name of the Action of Agent with parameters stripped off
621 */
622 private static String getName(String command) {
623 if (command.indexOf(" ") < 0)
624 return command;
625
626 return command.substring(0, command.indexOf(" "));
627 }
628}
Note: See TracBrowser for help on using the repository browser.