source: trunk/src/org/expeditee/gui/AttributeUtils.java@ 80

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

Added some more unit tests
Did a bunch of refactoring
AND added a few new features... @b @v were the most significant

File size: 22.3 KB
Line 
1package org.expeditee.gui;
2
3import java.awt.Color;
4import java.awt.Font;
5import java.awt.Point;
6import java.lang.reflect.InvocationTargetException;
7import java.lang.reflect.Method;
8import java.util.HashMap;
9import java.util.LinkedList;
10import java.util.List;
11
12import org.expeditee.io.Conversion;
13import org.expeditee.items.Item;
14import org.expeditee.items.Text;
15
16/**
17 * This class provides the methods to extract and set attributes of Items and
18 * Frames. These methods are called when a user merges a text item with
19 * <code>Attribute: Value</code> pairs.
20 *
21 * @author jdm18
22 *
23 */
24public class AttributeUtils {
25
26 private static final char SEPARATOR_CHAR = ':';
27
28 private static final String SEPARATOR_STRING = SEPARATOR_CHAR + " ";
29
30 private static final int GET_LENGTH = "get".length();
31
32 private static int SET_LENGTH = "set".length();
33
34 // List of method names to ignore when searching for a match
35 private static List<Method> _GetMethods = null;
36
37 private static HashMap<String, Method> _SetMethods = null;
38
39 // List of attributes which are ignored when copying.
40 private static List<String> _Ignore = null;
41
42 // List of method names to show in extraced lists even when they return
43 // null
44 // (Null is often used to indicate the default value is used)
45 private static List<Method> _AllowNull = null;
46
47 private static List<String> _ExtractIgnore = null;
48
49 // private static HashMap<String, String> _Abbreviations = null;
50
51 /**
52 * Initialises the _Ignore and _AllowNull lists.
53 */
54 private static void initLists() {
55
56 Class[] param = {};
57
58 try {
59 _Ignore = new LinkedList<String>();
60 _Ignore.add("date");
61 _Ignore.add("datecreated");
62 _Ignore.add("d");
63 _Ignore.add("link");
64 _Ignore.add("l");
65 _Ignore.add("action");
66 _Ignore.add("a");
67 _Ignore.add("position");
68 _Ignore.add("pos");
69 _Ignore.add("p");
70 _Ignore.add("x");
71 _Ignore.add("y");
72
73 _ExtractIgnore = new LinkedList<String>();
74 _ExtractIgnore.add("x");
75 _ExtractIgnore.add("y");
76
77 _AllowNull = new LinkedList<Method>();
78 _AllowNull.add(Item.class.getMethod("getColor", param));
79
80 _AllowNull.add(Frame.class.getMethod("getBackgroundColor", param));
81 _AllowNull.add(Frame.class.getMethod("getForegroundColor", param));
82
83 _GetMethods = new LinkedList<Method>();
84 _GetMethods.add(Item.class.getMethod("getDateCreated", param));
85
86 _GetMethods.add(Item.class.getMethod("getColor", param));
87 _GetMethods.add(Item.class.getMethod("getBackgroundColor", param));
88 _GetMethods.add(Item.class.getMethod("getAction", param));
89 _GetMethods.add(Item.class.getMethod("getData", param));
90 _GetMethods.add(Item.class.getMethod("getLink", param));
91 _GetMethods.add(Item.class.getMethod("getFillColor", param));
92 _GetMethods.add(Item.class.getMethod("getFillPattern", param));
93 _GetMethods.add(Item.class.getMethod("getThickness", param));
94
95 _GetMethods.add(Item.class.getMethod("getOwner", param));
96 _GetMethods.add(Item.class.getMethod("getLinkMark", param));
97 _GetMethods.add(Item.class.getMethod("getActionMark", param));
98
99 _GetMethods
100 .add(Item.class.getMethod("getActionCursorEnter", param));
101 _GetMethods
102 .add(Item.class.getMethod("getActionCursorLeave", param));
103 _GetMethods.add(Item.class.getMethod("getActionEnterFrame", param));
104 _GetMethods.add(Item.class.getMethod("getActionLeaveFrame", param));
105
106 _GetMethods.add(Item.class.getMethod("getTopShadowColor", param));
107 _GetMethods
108 .add(Item.class.getMethod("getBottomShadowColor", param));
109 _GetMethods.add(Item.class.getMethod("getArrow", param));
110
111 _GetMethods.add(Item.class.getMethod("getLinePattern", param));
112 _GetMethods.add(Item.class.getMethod("getLinkFrameset", param));
113 _GetMethods.add(Item.class.getMethod("getLinkTemplate", param));
114 _GetMethods.add(Item.class.getMethod("getPosition", param));
115 _GetMethods.add(Item.class.getMethod("getX", param));
116 _GetMethods.add(Item.class.getMethod("getY", param));
117
118 _GetMethods.add(Text.class.getMethod("getFamily", param));
119 _GetMethods.add(Text.class.getMethod("getFontStyle", param));
120 _GetMethods.add(Text.class.getMethod("getJustification", param));
121 _GetMethods.add(Text.class.getMethod("getWidth", param));
122 _GetMethods.add(Item.class.getMethod("getSize", param));
123
124 _GetMethods.add(Frame.class.getMethod("getOwner", param));
125 _GetMethods.add(Frame.class.getMethod("getProtection", param));
126 _GetMethods.add(Frame.class.getMethod("getDateCreated", param));
127 _GetMethods.add(Frame.class.getMethod("getLastModifyUser", param));
128 _GetMethods.add(Frame.class.getMethod("getLastModifyDate", param));
129 _GetMethods.add(Frame.class.getMethod("getForegroundColor", param));
130 _GetMethods.add(Frame.class.getMethod("getBackgroundColor", param));
131
132 Class[] pPoint = { Point.class };
133 Class[] pString = { String.class };
134 Class[] pInt = { int.class };
135 Class[] pFloat = { float.class };
136 Class[] pColor = { Color.class };
137 Class[] pBool = { boolean.class };
138 Class[] pArrow = { int.class, double.class };
139 Class[] pList = { List.class };
140 Class[] pIntArray = { int[].class };
141
142 _SetMethods = new HashMap<String, Method>();
143 _SetMethods.put("x", Item.class.getMethod("setX", pInt));
144 _SetMethods.put("y", Item.class.getMethod("setY", pInt));
145 _SetMethods.put("position", Item.class.getMethod("setPosition",
146 pPoint));
147 _SetMethods.put("p", Item.class.getMethod("setPosition", pPoint));
148 _SetMethods.put("pos", Item.class.getMethod("setPosition", pPoint));
149 _SetMethods.put("thickness", Item.class.getMethod("setThickness",
150 pFloat));
151 _SetMethods.put("t", Item.class.getMethod("setThickness", pFloat));
152
153 _SetMethods.put("color", Item.class.getMethod("setColor", pColor));
154 _SetMethods.put("c", Item.class.getMethod("setColor", pColor));
155
156 _SetMethods.put("backgroundcolor", Item.class.getMethod(
157 "setBackgroundColor", pColor));
158 _SetMethods.put("bgc", Item.class.getMethod("setBackgroundColor",
159 pColor));
160
161 _SetMethods
162 .put("action", Item.class.getMethod("setActions", pList));
163 _SetMethods.put("a", Item.class.getMethod("setActions", pList));
164 _SetMethods.put("d", Item.class.getMethod("setData", pList));
165 _SetMethods.put("data", Item.class.getMethod("setData", pList));
166
167 _SetMethods.put("link", Item.class.getMethod("setLink", pString));
168 _SetMethods.put("l", Item.class.getMethod("setLink", pString));
169
170 _SetMethods.put("fillcolor", Item.class.getMethod("setFillColor",
171 pColor));
172 _SetMethods.put("fc", Item.class.getMethod("setFillColor", pColor));
173
174 _SetMethods.put("fillpattern", Item.class.getMethod(
175 "setFillPattern", pString));
176 _SetMethods.put("fp", Item.class.getMethod("setFillPattern",
177 pString));
178
179 _SetMethods.put("owner", Item.class.getMethod("setOwner", pString));
180 _SetMethods.put("linkmark", Item.class.getMethod("setLinkMark",
181 pBool));
182 _SetMethods.put("lm", Item.class.getMethod("setLinkMark", pBool));
183 _SetMethods.put("actionmark", Item.class.getMethod("setActionMark",
184 pBool));
185 _SetMethods.put("am", Item.class.getMethod("setActionMark", pBool));
186
187 _SetMethods.put("actioncursorenter", Item.class.getMethod(
188 "setActionCursorEnter", pList));
189 _SetMethods.put("actioncursorleave", Item.class.getMethod(
190 "setActionCursorLeave", pList));
191 _SetMethods.put("actionenterframe", Item.class.getMethod(
192 "setActionEnterFrame", pList));
193 _SetMethods.put("actionleaveframe", Item.class.getMethod(
194 "setActionLeaveFrame", pList));
195
196 _SetMethods.put("topshadow", Item.class.getMethod(
197 "setTopShadowColor", pColor));
198 _SetMethods.put("bottomshadow", Item.class.getMethod(
199 "setBottomShadowColor", pColor));
200 _SetMethods.put("arrow", Item.class.getMethod("setArrow", pArrow));
201
202 _SetMethods.put("linepattern", Item.class.getMethod(
203 "setLinePattern", pIntArray));
204 _SetMethods.put("lp", Item.class.getMethod("setLinePattern",
205 pIntArray));
206
207 _SetMethods.put("linkframeset", Item.class.getMethod(
208 "setLinkFrameset", pString));
209 _SetMethods.put("lf", Item.class.getMethod("setLinkFrameset",
210 pString));
211 _SetMethods.put("linktemplate", Item.class.getMethod(
212 "setLinkTemplate", pString));
213 _SetMethods.put("lt", Item.class.getMethod("setLinkTemplate",
214 pString));
215
216 _SetMethods.put("family", Text.class
217 .getMethod("setFamily", pString));
218 _SetMethods.put("face", Text.class.getMethod("setFontStyle",
219 pString));
220 _SetMethods.put("fontstyle", Text.class.getMethod("setFontStyle",
221 pString));
222 _SetMethods.put("justification", Text.class.getMethod(
223 "setJustification", pInt));
224 _SetMethods.put("width", Text.class.getMethod("setWidth", pInt));
225 _SetMethods.put("size", Item.class.getMethod("setSize", pInt));
226 _SetMethods.put("s", Item.class.getMethod("setSize", pInt));
227
228 _SetMethods.put("foregroundcolor", Frame.class.getMethod(
229 "setForegroundColor", pColor));
230 _SetMethods.put("fgc", Frame.class.getMethod("setForegroundColor",
231 pColor));
232 _SetMethods.put("backgroundcolor0", Frame.class.getMethod(
233 "setBackgroundColor", pColor));
234 _SetMethods.put("bgc0", Frame.class.getMethod("setBackgroundColor",
235 pColor));
236 _SetMethods.put("protection", Frame.class.getMethod(
237 "setProtection", pString));
238
239 } catch (SecurityException e) {
240 // TODO Auto-generated catch block
241 e.printStackTrace();
242 } catch (NoSuchMethodException e) {
243 // TODO Auto-generated catch block
244 e.printStackTrace();
245 }
246 }
247
248 /**
249 * Extracts a list of attributes from the given Object. Any method that
250 * starts with <code>get</code>, takes no arguments and is not found in
251 * the Ignore list will be run, All the attributes are then put into a Text
252 * Item of the form <Name>:<Value> If the value returned by the get method
253 * is null, then the attribute will not be included, unless the name of the
254 * method is found in the AllowNull list.
255 *
256 * @param toExtract
257 * The Object from which to extract the attributes
258 * @return A Text Item containing the extracted Attributes.
259 */
260 public static Item ExtractAttributes(Object toExtract) {
261 if (toExtract == null)
262 return null;
263
264 // ensure the lists are populated
265 if (_Ignore == null)
266 initLists();
267
268 // StringBuffer to store all the extracted Attribute:Value pairs
269 StringBuffer attributes = new StringBuffer();
270
271 // iterate through the list of methods
272 for (Method m : _GetMethods) {
273
274 // Make sure the classes of the methods match the item
275 if (m.getDeclaringClass() == toExtract.getClass()
276 || m.getDeclaringClass() == toExtract.getClass()
277 .getSuperclass()) {
278 try {
279 Object o = m.invoke(toExtract, (Object[]) null);
280
281 if (o == null) {
282 // methods that return null are only included if they
283 // are in the AllowNull list
284 if (_AllowNull.contains(m)) {
285 String name = m.getName().substring(
286 GET_LENGTH).toLowerCase();
287 if (name.equals("color"))
288 o = "default";
289 else if (name.equals("backgroundcolor"))
290 o = "transparent";
291 else if (name.equals("foregroundcolor"))
292 o = "auto";
293 else
294 o = "";
295 } else {
296 continue;
297 }
298 }
299 // skip methods that are in the ignore lists
300 if (_ExtractIgnore.contains(m.getName().substring(
301 GET_LENGTH).toLowerCase())) {
302 continue;
303 }
304
305 if (o instanceof Integer) {
306 Integer i = (Integer)o;
307 if (i == Item.DEFAULT_INTEGER)
308 continue;
309 if (m.getName().endsWith("Justification")
310 && convertJustificationToString((Integer) o) != null)
311 o = convertJustificationToString((Integer) o);
312 // -1 indicates default value
313 else
314 o = i;
315 } else if (o instanceof Float) {
316 // -1 indicates default value
317 if (((Float) o) < 0.0001)
318 continue;
319 } else if (o instanceof Double) {
320 // -1 indicates default value
321 if (((Double) o) < 0.0001)
322 continue;
323 } else if (o instanceof Color) {
324 // converts the color to the Expeditee code
325 o = Conversion.getExpediteeColorCode((Color) o);
326 if (o == null)
327 continue;
328 } else if (o instanceof Point) {
329 Point p = (Point) o;
330 o = (int) p.getX() + " " + (int) p.getY();
331 } else if (o instanceof Font) {
332 Font f = (Font) o;
333
334 String s = f.getName() + "-";
335 if (f.isPlain())
336 s += "Plain";
337
338 if (f.isBold())
339 s += "Bold";
340
341 if (f.isItalic())
342 s += "Italic";
343
344 s += "-" + f.getSize();
345 o = s;
346 } else if (o instanceof Text) {
347 o = ((Text) o).getFirstLine();
348 } else if (o instanceof List) {
349 List list = (List) o;
350 for (Object ob : list)
351 attributes
352 .append(m.getName().substring(GET_LENGTH))
353 .append(SEPARATOR_STRING).append(ob)
354 .append("\n");
355 continue;
356 } else if (o instanceof int[]) {
357 StringBuffer sb = new StringBuffer();
358 int[] values = (int[]) o;
359 for (int i = 0; i < values.length; i++) {
360 sb.append(values[i]).append(' ');
361 }
362 sb.deleteCharAt(attributes.length() - 1);
363 o = sb.toString();
364 } else if (o instanceof Boolean) {
365 // true is the default for boolean values
366 if (((Boolean) o).booleanValue())
367 continue;
368 }
369 //Append the attributes
370 attributes.append(m.getName().substring(GET_LENGTH))
371 .append(SEPARATOR_STRING).append(o).append("\n");
372 } catch (Exception e) {
373 // TODO Auto-generated catch block
374 e.printStackTrace();
375 }
376 }
377 }
378
379 // if no attributes were extracted
380 if (attributes.length() <= 0)
381 return null;
382
383 while (attributes.charAt(attributes.length() - 1) == '\n')
384 attributes.delete(attributes.length() - 1, attributes.length());
385
386 // create the text Item
387 Frame current = DisplayIO.getCurrentFrame();
388 Item attribs = current.getStatsTextItem(attributes.toString());
389 return attribs;
390 }
391
392 /**
393 * Returns the String name of the justification value given. just should
394 * correspond to one of the Item.JUSTIFICATION constants, if it does not
395 * then null is returned.
396 *
397 * @param just
398 * The justification level as defined by the Item.JUSTIFICATION
399 * constants.
400 * @return The String name (i.e. "Center") of the justification given, or
401 * null if no match is found.
402 */
403 private static String convertJustificationToString(int just) {
404 switch (just) {
405 case Item.JUSTIFICATION_CENTER:
406 return "Center";
407 case Item.JUSTIFICATION_FULL:
408 return "Full";
409 case Item.JUSTIFICATION_LEFT:
410 return "Left";
411 case Item.JUSTIFICATION_RIGHT:
412 return "Right";
413 default:
414 return null;
415 }
416 }
417
418 /**
419 * Attempts to set the attribute in the given attribute: value pair. The
420 * value string should be formatted as follows:
421 * <code> Attribute: Value </code> Multiple values can be used if they are
422 * separated by spaces
423 *
424 * @param toSet
425 * The Item to set the attribute of
426 * @param attribs
427 * The Text item that contains the list of attributes to set
428 * @return True if the attribute(s) were sucessfully set, false otherwise
429 */
430 public static boolean SetAttribute(Object toSet, Text attribs) {
431 // error checking
432 if (toSet == null || attribs == null)
433 return false;
434
435 if (_Ignore == null)
436 initLists();
437
438 // get the list of attribute: value pairs
439 List<String> values = attribs.getTextList();
440 // if no paris exist, we are done
441 if (values.size() == 0
442 || (values.size() == 1 && values.get(0).length() == 0))
443 return false;
444
445 // loop through all attribute: value pairs
446 for (int i = 0; i < values.size(); i++) {
447 StringBuffer v = new StringBuffer(values.get(i));
448
449 // remove the annotation mark (if applicable)
450 if (v.indexOf("@") == 0)
451 v = v.deleteCharAt(0);
452
453 // check if the next string is another attribute to merge or a
454 // continuation
455 while (i < values.size() - 1) {
456 StringBuffer next = new StringBuffer(values.get(i + 1));
457
458 // if the next String has a colon, then it may be another
459 // attribute
460 if (next.indexOf("" + SEPARATOR_CHAR) >= 0) {
461 // if the attribute is the same as v, then it is a
462 // continuation
463 if (v.indexOf(getAttribute(next.toString())) == 0) {
464 // strip the attribute from next
465 next = new StringBuffer(getValue(next.toString()));
466 // if the attribute is not the same, then it may be a
467 // new method
468 } else {
469 // if this is indeed a method, then leave the while loop
470 // if(_SetMethods.containsKey(StripFromColon(next.toString()).toLowerCase())){
471 break;
472 // }
473 }
474 }
475
476 v.append("\n").append(next);
477 i++;
478 }
479
480 if (v.length() > 0
481 && !SetAttribute(toSet, v.toString(), values.size() > 1)) {
482 // if no other attributes have been set
483 if (i == 0)
484 return false;
485 // otherwise, this is a list of attributes, so continue
486 else {
487 String stripped = getAttribute(v.toString());
488 if (stripped == null) {
489 // This happens when there is an attribute at the start
490 // Then a bunch of plain text
491 return false;
492 } else if (_Ignore.contains(stripped)) {
493 return false;
494 } else if (!(_SetMethods.containsKey(stripped))) {
495 // Display an error message if its not in our list of
496 // attributes to ignore when copying
497 FrameGraphics.WarningMessage("Attribute: '"
498 + getAttribute(v.toString())
499 + "' does not exist.");
500 } else {
501 String types = "";
502 for (Class c : _SetMethods.get(stripped)
503 .getParameterTypes())
504 types += c.getSimpleName() + " ";
505 FrameGraphics.WarningMessage("Wrong arguments for: '"
506 + getAttribute(v.toString()) + "' expecting "
507 + types.trim() + " found '"
508 + getValue(v.toString()) + "'");
509 }
510 }
511 } else if (v.length() == 0)
512 return false;
513 }
514
515 return true;
516 }
517
518 private static boolean SetAttribute(Object toSet, String value,
519 boolean isAttributeList) {
520 // separate attribute and value from string
521 String attribute = getAttribute(value);
522 value = getValue(value);
523
524 // if there was no colon
525 if (value == null || attribute == null)
526 return false;
527
528 if (value.length() == 0)
529 value = "";
530
531 attribute = attribute.toLowerCase();
532
533 // Some properties are ignored when multiple attributes are being set on
534 // an item at the same time
535 if (isAttributeList && _Ignore.contains(attribute)) {
536 // System.out.println("Attribute ignored: " + attribute);
537 return true;
538 }
539
540 // separate multiple values if required
541 Method toRun = _SetMethods.get(attribute);
542
543 // if this is not the name of a method, it may be the name of an agent
544 if (toRun == null) {
545 // System.out.println("Attrib not found for: " + attribute);
546 return false;
547 }
548
549 // if there are duplicate methods with the same name
550 List<Method> possibles = new LinkedList<Method>();
551 if (toRun.getDeclaringClass().isInstance(toSet))
552 possibles.add(toRun);
553 int i = 0;
554 while (_SetMethods.containsKey(attribute + i)) {
555 if (_SetMethods.get(attribute + i).getDeclaringClass() == toSet
556 .getClass()
557 || _SetMethods.get(attribute + i).getDeclaringClass() == toSet
558 .getClass().getSuperclass())
559 possibles.add(_SetMethods.get(attribute + i));
560 i++;
561 }
562
563 for (Method possible : possibles) {
564 Object current = null;
565 Object[] param = {};
566 Class toSetClass = toSet.getClass();
567 Class toSetSuperClass = toSetClass.getSuperclass();
568 // find the corresponding get method for this set method
569 // and get the current value of the attribute
570 for (Method m : _GetMethods) {
571 if ((m.getDeclaringClass() == toSetClass || m
572 .getDeclaringClass() == toSetSuperClass)
573 && m.getName().substring(GET_LENGTH).equals(
574 possible.getName().substring(SET_LENGTH))) {
575 try {
576 current = m.invoke(toSet, param);
577 } catch (IllegalArgumentException e) {
578 // TODO Auto-generated catch block
579 e.printStackTrace();
580 } catch (IllegalAccessException e) {
581 // TODO Auto-generated catch block
582 e.printStackTrace();
583 } catch (InvocationTargetException e) {
584 // TODO Auto-generated catch block
585 e.printStackTrace();
586 }
587 break;
588 }
589
590 }
591
592 try {
593 Object[] params = Conversion.Convert(possible, value, current);
594
595 try {
596 possible.invoke(toSet, params);
597 return true;
598 } catch (IllegalArgumentException e) {
599 // TODO Auto-generated catch block
600 e.printStackTrace();
601 } catch (IllegalAccessException e) {
602 // TODO Auto-generated catch block
603 e.printStackTrace();
604 } catch (InvocationTargetException e) {
605 FrameGraphics.DisplayMessage(toSet.getClass()
606 .getSimpleName()
607 + " type does not support that attribute.");
608 // e.printStackTrace();
609 }
610 } catch (NumberFormatException e) {
611
612 }
613 }
614
615 return false;
616
617 }
618
619 /**
620 * Returns the part of the given string that is after the attribute value
621 * pair separator. If that character is not there it returns the empty
622 * string if it is an annotation item or the entire string if it is not.
623 *
624 * @param attributeValuePair
625 * the string to get the value from.
626 * @return the value from the attribute value pair.
627 */
628 public static String getValue(String toStrip) {
629 assert (toStrip != null);
630 if (toStrip.length() == 0)
631 return "";
632
633 int ind = toStrip.lastIndexOf(SEPARATOR_CHAR);
634
635 if (ind < 0) {
636 // If it is an annotation item return the empty string
637 // Annotation items can not be values only
638 if (toStrip.charAt(0) == '@') {
639 return "";
640 }
641 // It is a value with no attribute.
642 return toStrip;
643 }
644
645 return toStrip.substring(ind + 1).trim();
646 }
647
648 /**
649 * Returns the part of the given string that is before the attribute value
650 * pair separator, or null if the given String does not include the
651 * separator.
652 *
653 * @param attributeValuePair
654 * The String to strip
655 * @return the attribute if there is one or null if there is not
656 */
657 public static String getAttribute(String attributeValuePair) {
658 int ind = attributeValuePair.indexOf(SEPARATOR_CHAR);
659 if (ind < 0)
660 return null;
661
662 attributeValuePair = attributeValuePair.substring(0, ind);
663 attributeValuePair = attributeValuePair.trim();
664
665 if (attributeValuePair.length() == 0)
666 return null;
667
668 return attributeValuePair;
669 }
670
671 public static void setSingleValue(Text text, String value) {
672 assert (value != null);
673
674 String oldText = text.getText();
675 String attribute = getAttribute(oldText);
676
677 if (attribute == null)
678 attribute = text.getText().trim();
679
680 text.setText(attribute + SEPARATOR_STRING + value);
681 }
682
683 public static Double getDoubleValue(String attributeValuePair) {
684 String value = getValue(attributeValuePair);
685
686 if (value == null)
687 return null;
688
689 try {
690 return Double.parseDouble(value);
691 } catch (Exception e) {
692 }
693 return null;
694 }
695}
Note: See TracBrowser for help on using the repository browser.