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

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

Added calculate action

File size: 19.9 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 private static void LoadMethods(Class c) {
235 // list of methods to test
236 Method[] toLoad = c.getMethods();
237
238 for (Method m : toLoad) {
239 // only allow methods with the right modifiers
240 if (MethodCheck(m)) {
241 String lowercaseName = m.getName().toLowerCase();
242 if (!(_Actions.containsKey(lowercaseName)))
243 _Actions.put(lowercaseName, m);
244 else {
245 int i = 0;
246 while (_Actions.containsKey(lowercaseName + i))
247 i++;
248
249 _Actions.put(lowercaseName + i, m);
250 }
251
252 }
253 }
254 }
255
256 /**
257 * Checks if the given Method corresponds to the restrictions of Action
258 * commands, namely: Declared (not inherited), Public, and Static, with a
259 * void return type.
260 *
261 * @param m
262 * The Method to check
263 * @return True if the Method meets the above conditions, false otherwise.
264 */
265 private static boolean MethodCheck(Method m) {
266 // the return type must be void
267 if (m.getReturnType() != void.class)
268 return false;
269
270 int mods = m.getModifiers();
271
272 // check the method is declared (not inherited)
273 if ((mods & Method.DECLARED) != Method.DECLARED)
274 return false;
275
276 // check the method is public
277 if ((mods & Modifier.PUBLIC) != Modifier.PUBLIC)
278 return false;
279
280 // check the method is static
281 if ((mods & Modifier.STATIC) != Modifier.STATIC)
282 return false;
283
284 // if we have not returned yet, then the tests have all passed
285 return true;
286 }
287
288 /**
289 * Performs the given action command. The source Frame and Item are given
290 * because they are required by some actions. Note that the source frame
291 * does not have to be the Item's parent Frame.
292 *
293 * @param source
294 * The Frame that the action should apply to
295 * @param launcher
296 * The Item that has the action assigned to it
297 * @param command
298 * The action to perform
299 */
300 public static void PerformAction(Frame source, Item launcher, String command) {
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 if (command.length() > mname.length())
308 command = command.substring(mname.length() + 1);
309 else
310 command = "";
311
312 // Strip off the @ from annotation items
313 if (mname.startsWith("@"))
314 mname = mname.substring(1);
315
316 mname = mname.trim();
317 String lowercaseName = mname.toLowerCase();
318 // check for protection on frame
319 if (ItemUtils.ContainsTag(source.getItems(), "@No" + mname)) {
320 MessageBay.displayMessage("Frame is protected by @No" + mname
321 + " tag.");
322 return;
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;
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 possible.invoke(null, parameters);
363 return;
364 } catch (Exception e) {
365 Logger.Log(e);
366 e.printStackTrace();
367 }
368 }
369 if (possibles.size() > 0) {
370 MessageBay.errorMessage("Incorrect parameters for " + mname);
371 } else {
372 assert (false);
373 MessageBay.errorMessage(mname + " action not found");
374 }
375 }
376
377 /**
378 * Launches an agent with the given name, and passes in the given parameters
379 *
380 * @param name
381 * The name of the JAG to load
382 * @param parameters
383 * The parameters to pass to the JAG
384 * @param source
385 * The starting Frame that the JAG is being launched on
386 */
387 private static void LaunchAgent(String name, String parameters,
388 Frame source, Item clicked) {
389 // Use the correct case version for printing error messages
390 String nameWithCorrectCase = name;
391 name = name.toLowerCase();
392 // save the current frame (if necesssary)
393 // TODO make this nicer... ie. make Format an action rather than an
394 // agent and save frames only before running agents
395 if (!name.equalsIgnoreCase("format") && !name.equalsIgnoreCase("sort")) {
396 FrameUtils.LeavingFrame(source);
397 }
398
399 try {
400 // check for stored capitalisation
401 if (_JAGs.containsKey(name.toLowerCase())) {
402 name = _JAGs.get(name.toLowerCase());
403 } else if (name.toLowerCase().endsWith("tree")) {
404 parameters = name.substring(0, name.length() - "tree".length())
405 + " " + parameters;
406 name = "WriteTree";
407 } else if (name.toLowerCase().endsWith("frame")) {
408 parameters = name
409 .substring(0, name.length() - "frame".length())
410 + " " + parameters;
411 name = "WriteFrame";
412 }
413
414 // load the JAG class
415 Class agentClass = Class.forName(AGENTS_PACKAGE + name);
416
417 // get the constructor for the JAG class
418 Constructor con = null;
419 Constructor[] constructors = agentClass.getConstructors();
420 Object[] params = null;
421
422 // determine correct parameters for constructor
423 for (Constructor c : constructors) {
424 if (parameters.length() > 0
425 && c.getParameterTypes().length == 1) {
426 con = c;
427 params = new String[1];
428 params[0] = parameters;
429 break;
430 } else if (c.getParameterTypes().length == 0 && con == null) {
431 con = c;
432 }
433 }
434
435 // if there is no constructor, return
436 if (con == null) {
437 MessageBay.displayMessage(INVALID_PARAMETERS_ERROR
438 + nameWithCorrectCase);
439 // System.out.println("Constructor not found...");
440 return;
441 }
442
443 // create the JAG
444 _Agent = (Agent) con.newInstance(params);
445
446 Thread t = new Thread(_Agent);
447 t.setPriority(Thread.MIN_PRIORITY);
448
449 Item itemParam = clicked;
450 if (FreeItems.textAttachedToCursor()) {
451 itemParam = FreeItems.getItemAttachedToCursor();
452 }
453
454 // check for errors during initialisation
455 if (!_Agent.initialise(source, itemParam)) {
456 MessageBay.errorMessage("Error initialising agent: "
457 + nameWithCorrectCase);
458 _Agent = null;
459 return;
460 }
461
462 t.start();
463
464 // if there is a completion frame, then display it to the user
465 if (_Agent.hasResultFrame()) {
466 Frame next = _Agent.getResultFrame();
467 FrameUtils.DisplayFrame(next, true);
468 }
469
470 } catch (ClassNotFoundException cnf) {
471 MessageBay.errorMessage(nameWithCorrectCase
472 + "' is not an action or agent.");
473 } catch (Exception e) {
474 MessageBay.errorMessage("Error creating Agent: '"
475 + nameWithCorrectCase + "'");
476 System.out.println("Agent set to Null.");
477 _Agent = null;
478 e.printStackTrace();
479 Logger.Log(e);
480 }
481 FrameGraphics.refresh(false);
482
483 return;
484 }
485
486 /**
487 * Used to determine if the previously launched agent is still executing.
488 *
489 * @return True if the last Agent is still executing, False otherwise.
490 */
491 public static boolean isAgentRunning() {
492 if (_Agent != null)
493 return _Agent.isRunning();
494
495 return false;
496 }
497
498 /**
499 * Stops the currently running Agent (If there is one) by calling
500 * Agent.stop(). Note: This may not stop the Agent immediately, but the
501 * Agent should terminate as soon as it is safe to do so.
502 */
503 public static void stopAgent() {
504 if (_Agent != null && _Agent.isRunning()) {
505 MessageBay.errorMessage("Stopping Agent...");
506 _Agent.stop();
507 }
508 }
509
510 public static void interruptAgent() {
511 if (_Agent != null) {
512 _Agent.interrupt();
513 }
514 }
515
516 /**
517 * Converts the given String of values into an array of Objects
518 *
519 * @param launcher
520 * The Item used to launch the action, it may be required as a
521 * parameter
522 * @param values
523 * A list of space separated String values to convert to objects
524 * @return The created array of Objects
525 */
526 public static Object[] CreateObjects(Method method, Frame source,
527 Item launcher, String values) {
528 // The parameter types that should be created from the given String
529 Class[] paramTypes = method.getParameterTypes();
530
531 int paramCount = paramTypes.length;
532 // if the method has no parameters
533 if (paramCount == 0)
534 return new Object[0];
535
536 Object[] objects = new Object[paramCount];
537 int ind = 0;
538
539 // if the first class in the list is a frame or item, it is the source
540 // or launcher
541 // length must be at least one if we are still running
542 if (paramTypes[ind] == Frame.class) {
543 objects[ind] = source;
544 ind++;
545 }
546
547 if (paramCount > ind && Item.class.isAssignableFrom(paramTypes[ind])) {
548 objects[ind] = launcher;
549 ind++;
550 }
551
552 String param = values;
553 // convert the rest of the objects
554 for (; ind < objects.length; ind++) {
555 // check if its the last param and combine
556 if (values.length() > 0 && ind == objects.length - 1) {
557 param = values.trim();
558 // check if its a string
559 if (param.length() > 0 && param.charAt(0) == '"') {
560 int endOfString = param.indexOf('"', 1);
561 if (endOfString > 0) {
562 param = param.substring(0, endOfString);
563 }
564 }
565 } else {// strip off the next value
566 param = ParseValue(values);
567 values = RemainingParams(values);
568 }
569 // convert the value to an object
570 try {
571 Object o = Conversion.Convert(paramTypes[ind], param);
572 if (o == null)
573 return null;
574 objects[ind] = o;
575 } catch (Exception e) {
576 return null;
577 }
578 }
579
580 return objects;
581 }
582
583 /**
584 * Returns a string containing the remaining params after ignoring the first
585 * one.
586 *
587 * @param params
588 * a space sparated list of N parameters
589 * @return the remaining N - 1 parameters
590 */
591 public static String RemainingParams(String params) {
592 if (params.length() == 0)
593 return null;
594
595 // remove leading and trailing spaces
596 params = params.trim();
597
598 // if there are no more parameters, we are done
599 if (params.indexOf(" ") < 0) {
600 return "";
601 }
602
603 // Check if we have a string parameter
604 if (params.charAt(0) == '"') {
605 int endOfString = params.indexOf('"', 1);
606 if (endOfString > 0) {
607 if (endOfString > params.length())
608 return "";
609 return params.substring(endOfString + 1).trim();
610 }
611 }
612
613 return params.substring(params.indexOf(" ")).trim();
614 }
615
616 /**
617 * Returns the first value in the space separated String of parameters
618 * passed in. Strings are enclosed in double quotes.
619 *
620 * @param params
621 * The String of space separated values
622 * @return The first value in the String
623 */
624 public static String ParseValue(String params) {
625 if (params.length() == 0)
626 return null;
627
628 // remove leading and trailing spaces
629 String param = params.trim();
630
631 // Check if we have a string parameter
632 if (param.charAt(0) == '"') {
633 int endOfString = param.indexOf('"', 1);
634 if (endOfString > 0)
635 return param.substring(1, endOfString);
636 }
637
638 // if there are no more parameters, we are done
639 if (param.indexOf(" ") < 0) {
640 return param;
641 }
642
643 return param.substring(0, param.indexOf(" "));
644 }
645
646 /**
647 * Separates the name of the given command from any parameters and returns
648 * them
649 *
650 * @param command
651 * The String to separate out the Action or Agent name from
652 * @return The name of the Action of Agent with parameters stripped off
653 */
654 private static String getName(String command) {
655 if (command.indexOf(" ") < 0)
656 return command;
657
658 return command.substring(0, command.indexOf(" "));
659 }
660
661 /**
662 * Gets an uncapitalized font name and returns the capitalized font name.
663 * The capitalized form can be used with the Font.decoded method to get a
664 * corresponding Font object.
665 *
666 * @param fontName
667 * a font name in mixed case
668 * @return the correct capitalized form of the font name
669 */
670 public static String getCapitalizedFontName(String fontName) {
671 // Initialize the fonts if they have not already been loaded
672 initFonts();
673 return _Fonts.get(fontName.toLowerCase());
674 }
675
676 /**
677 * Initialise the fontsList if it has not been done already
678 */
679 private static void initFonts() {
680 if (_Fonts.size() == 0) {
681 String[] availableFonts = GraphicsEnvironment
682 .getLocalGraphicsEnvironment()
683 .getAvailableFontFamilyNames();
684 for (String s : availableFonts) {
685 _Fonts.put(s.toLowerCase(), s);
686 }
687 }
688 }
689
690 public static HashMap<String, String> getFonts() {
691 initFonts();
692 return _Fonts;
693 }
694}
Note: See TracBrowser for help on using the repository browser.