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

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

Made a few minor changes...

Also

For interactive widgets the border will not be selected if the user is inside the enclosed area

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