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

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