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

Last change on this file since 275 was 275, checked in by bjn8, 16 years ago

Made load actions method public - so plugins can load their custom actions upon initialization

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