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

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

Added additional mail functionality...
Also added REMINDER feature

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