Source code: com/tripi/asp/JavaObjectNode.java
1 /**
2 * ArrowHead ASP Server
3 * This is a source file for the ArrowHead ASP Server - an 100% Java
4 * VBScript interpreter and ASP server.
5 *
6 * For more information, see http://www.tripi.com/arrowhead
7 *
8 * Copyright (C) 2002 Terence Haddock
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23 *
24 */
25 package com.tripi.asp;
26
27 import java.util.Hashtable;
28 import java.util.Vector;
29 import java.util.Map;
30 import java.lang.reflect.*;
31 import org.apache.log4j.Category;
32
33 /**
34 * JavaObjectNode class handles the interaction between Java objects
35 * and ASP objects.
36 * @author Terence Haddock
37 * @version 0.9
38 */
39 public class JavaObjectNode extends DefaultNode implements ObjectNode, MapNode
40 {
41 /** Debugging category */
42 private static Category DBG = Category.getInstance(JavaObjectNode.class);
43
44 /** Java object this ASP node is referencing */
45 Object obj;
46
47 /**
48 * Constructor
49 * @param obj Java object this ASP object is referencing
50 */
51 public JavaObjectNode(Object obj)
52 {
53 this.obj = obj;
54 }
55
56 /**
57 * getField obtains a field of a Java object. Returns a JavaFieldNode
58 * reference to this field.
59 * @param ident Identifier of field to obtain
60 * @see ObjectNode#getField(IdentNode)
61 */
62 public Object getField(IdentNode ident)
63 {
64 if (DBG.isDebugEnabled())
65 DBG.debug("getField: " + ident);
66 String name = ident.ident;
67 return new JavaFieldNode(obj, ident);
68 }
69
70 /**
71 * Obtains the Java sub-object contained within this ASP node.
72 * @return Java sub-object
73 */
74 public Object getSubObject()
75 {
76 return obj;
77 }
78
79 /**
80 * Sets the value of this Java object, if the Java object implements
81 * the SimpleReference object.
82 * @param value Value to set
83 * @throws AspException if an error occurs.
84 */
85 public void setValue(Object value) throws AspException
86 {
87 if (obj instanceof SimpleReference) {
88 ((SimpleReference)obj).setValue(value);
89 } else {
90 throw new AspReadOnlyException();
91 }
92 }
93
94 /**
95 * Obtains an array index, or calls a function, of a Java object.
96 * @param varlist Variable list of parameters.
97 * @param context AspContext under which this Java object was called.
98 * @return Value of the array or return value of the function.
99 * @throws AspException if an exception occurs.
100 * @see MapNode#getIndex(VarListNode,AspContext)
101 */
102 public Object getIndex(VarListNode varlist, AspContext context) throws AspException
103 {
104 /* See if this is a simple type of reference */
105 if (varlist.size() == 0 && obj instanceof SimpleReference)
106 {
107 if (DBG.isDebugEnabled())
108 DBG.debug("Trying simple reference");
109 Object subValue = ((SimpleReference)obj).getValue();
110 if (DBG.isDebugEnabled())
111 {
112 DBG.debug("Sub-value: " + subValue);
113 if (subValue != null)
114 DBG.debug("Sub-value class: " +
115 subValue.getClass());
116 }
117 /* Return it as a Node object */
118 return Types.coerceToNode(subValue);
119 }
120 /* Only single dimensional lists supported */
121 if (varlist.size() != 1) {
122 throw new AspInvalidArgumentsException(""+varlist.size());
123 }
124 /* Handle MapNode objects */
125 if (obj instanceof MapNode) {
126 return ((MapNode)obj).getIndex(varlist, context);
127 }
128 /* Handle Java arrays */
129 if (obj.getClass().isArray()) {
130 Vector vec = (Vector)varlist.execute(context);
131 Integer i = Types.coerceToInteger(vec.get(0));
132 Object sObj = Array.get(obj, i.intValue());
133 return Types.coerceToNode(sObj);
134 }
135 /* Map and SimpleMap objects are all that is left */
136 if (!(obj instanceof Map)&&!(obj instanceof SimpleMap)) {
137 throw new AspException("Invalid argument: " + obj.getClass());
138 }
139 Vector vec = (Vector)varlist.execute(context);
140 Object objVal = vec.get(0);
141 if (!(objVal instanceof Integer) && !(objVal instanceof String))
142 objVal = Types.coerceToString(objVal);
143 if (DBG.isDebugEnabled())
144 DBG.debug("getIndex(" + objVal + ")");
145 if (obj instanceof Map) {
146 return new JavaAccessorNode((Map)obj, objVal);
147 } else {
148 return new JavaAccessorNode((SimpleMap)obj, objVal);
149 }
150 }
151
152 /**
153 * Returns the upper bound of the array held within this JavaObjectNode.
154 * Supports only single-dimensional Java arrays of objects.
155 * @param dimension Which dimension to obtain
156 * @return upper bound
157 * @see MapNode#getUBOUND
158 * @throws AspSubscriptOutOfRangeException if dimension does not equal 1
159 */
160 public int getUBOUND(int dimension) throws AspException
161 {
162 if (dimension != 1) {
163 throw new AspSubscriptOutOfRangeException("UBOUND");
164 }
165 if (obj instanceof Object[]) {
166 return ((Object[])obj).length - 1;
167 }
168 throw new AspException("LBOUND called on non-array");
169 }
170
171 /**
172 * Returns the lower bound of the array contained within the Java object.
173 * @param dimension Which dimension to obtain.
174 * @return lower bound, usually 0
175 * @see MapNode#getLBOUND
176 * @throws AspSubscriptOutOfRangeExceptoin if dimension does not equal 1
177 */
178 public int getLBOUND(int dimension) throws AspException
179 {
180 if (dimension != 1) {
181 throw new AspSubscriptOutOfRangeException("UBOUND");
182 }
183 if (obj instanceof Object[]) {
184 return 0;
185 }
186 throw new AspException("LBOUND called on non-array");
187 }
188
189 /**
190 * Converts this JavaObjectNode to a string, for debugging.
191 * @return String representation of this Java object
192 */
193 public String toString()
194 {
195 if (obj == null)
196 {
197 return "{JavaObjectNode=null}";
198 } else {
199 return "{JavaObjectNode=" + obj.toString() + "(" + obj.getClass() + ")}";
200 }
201 }
202
203 /**
204 * JavaAccessorNode class handles the obtaining of object
205 * within Java objects by strings.
206 */
207 static class JavaAccessorNode extends DefaultNode implements SimpleReference
208 {
209 /** simpleMapObj is used if the sub-object is an instance of the
210 * SimpleMap interface.
211 */
212 SimpleMap simpleMapObj;
213
214 /**
215 * mapObj isused if the sub-object is an instance of the Map interface.
216 */
217 Map mapObj;
218
219 /**
220 * The key being obtained from the map.
221 */
222 Object key;
223
224 /**
225 * Constructor, Map form.
226 * @param obj Map object
227 * @param str String being referenced
228 */
229 public JavaAccessorNode(Map obj, Object key)
230 {
231 this.mapObj = obj;
232 this.key = key;
233 }
234
235 /**
236 * Constructor, SimpleMap form.
237 * @param obj SimpleMap object.
238 * @param str String being referenced.
239 */
240 public JavaAccessorNode(SimpleMap obj, Object key)
241 {
242 this.simpleMapObj = obj;
243 this.key = key;
244 }
245
246 /**
247 * getValue is a SimpleReference method which obtains the value
248 * of the current reference.
249 * @return value of the current reference
250 */
251 public Object getValue() throws AspException
252 {
253 Object ret;
254 if (mapObj != null) {
255 ret = Types.coerceToNode(mapObj.get(key));
256 if (DBG.isDebugEnabled())
257 if (ret != null)
258 DBG.debug("Getting value of " + mapObj + " = "
259 + ret + "(" + ret.getClass() + ")");
260 else
261 DBG.debug("Getting value of " + simpleMapObj + " = null");
262 } else {
263 ret = Types.coerceToNode(simpleMapObj.get(key));
264 if (DBG.isDebugEnabled())
265 if (ret != null)
266 DBG.debug("Getting value of " + simpleMapObj + " = " +
267 ret + "(" + ret.getClass() + ")");
268 else
269 DBG.debug("Getting value of " + simpleMapObj + " = null");
270 }
271 return ret;
272 }
273
274 /**
275 * setValue sets the value of the current reference.
276 * @param val New value
277 */
278 public void setValue(Object val) throws AspException
279 {
280 val = Types.dereference(val);
281 if (mapObj != null) {
282 mapObj.put(key, val);
283 } else {
284 simpleMapObj.put(key, val);
285 }
286 }
287 }
288
289 /**
290 * The JavaFieldNode class handles referencing fields within Java objects.
291 */
292 static class JavaFieldNode extends DefaultNode implements FunctionNode, SimpleReference
293 {
294 /** Global cache of data about classes */
295 static private Hashtable classData = new Hashtable();
296
297 /** The object being referenced by this class */
298 Object obj;
299
300 /** The identifier of the field being referenced */
301 IdentNode ident;
302
303 /** Vector of methods which match the identifier being referenced */
304 Vector matchingMethods;
305
306 /** Vector of fields which match the identifier being referenced */
307 Vector matchingFields;
308
309 /**
310 * Constructor.
311 * @param obj Object being referenced
312 * @param field IdentNode identifier of field being referenced
313 */
314 JavaFieldNode(Object obj, IdentNode field)
315 {
316 this.obj = obj;
317 this.ident = field;
318 matchingMethods = getMatchingMethods(obj.getClass(), field);
319 matchingFields = getMatchingFields(obj.getClass(), field);
320 }
321
322 /**
323 * Executes this field given a parameter list.
324 * @param vars Parameters to execute.
325 * @param context Current context
326 * @see FunctionNode#execute(VarListNode, AspContext)
327 */
328 public Object execute(VarListNode vars, AspContext context)
329 throws AspException
330 {
331 if (DBG.isDebugEnabled())
332 DBG.debug("Execute " + ident +
333 " on " + obj.getClass() + " with " +
334 vars.size() + " parameters");
335
336 Vector values = executeAndDereference(vars, context);
337 Method met = findMethod(values);
338
339 if (met == null) {
340 if (DBG.isDebugEnabled())
341 DBG.debug("Associated method " +
342 "not found, trying field access " +
343 "on " + obj);
344 /* Get the sub-object */
345 Object subObj = getValue();
346 /* Call the sub-object's function */
347 if (subObj instanceof MapNode) {
348 return ((MapNode)subObj).getIndex(vars, context);
349 }
350 throw new AspException("Unknown function: " + ident);
351 }
352 Object res = executeMethod(met, values);
353
354 Class paramTypes[] = met.getParameterTypes();
355 for (int i = 0; i < paramTypes.length; i++)
356 {
357 if (paramTypes[i] == ByRefValue.class) {
358 Object finalValue = values.get(i);
359 Object calledValue = vars.get(i);
360 if (calledValue instanceof IdentNode) {
361 context.setValue((IdentNode)calledValue, finalValue);
362 } else if (calledValue instanceof SimpleReference) {
363 ((SimpleReference)calledValue).setValue(finalValue);
364 } else {
365 /* Cannot set value */
366 }
367 }
368 }
369
370 return res;
371 }
372
373 /**
374 * Sets the value of a field.
375 * @param setObj new value of field.
376 * @throws AspException if an error occurs
377 * @see SimpleReference#setValue(Object)
378 */
379 public void setValue(Object setObj) throws AspException
380 {
381 if (matchingFields.size() == 0 && matchingMethods.size() == 0)
382 {
383 throw new AspException("Unknown field name: " + ident);
384 }
385 if (matchingFields.size()<1)
386 {
387 VarListNode vl = new VarListNode();
388 vl.append(setObj);
389 Vector vec = executeAndDereference(vl, null);
390 Method met = findMethod(vec);
391 if (met != null) {
392 executeMethod(met, vec);
393 return;
394 }
395 throw new AspException("Unknown field name: " + ident);
396 }
397 while (setObj instanceof JavaObjectNode) {
398 if (DBG.isDebugEnabled())
399 DBG.debug("Dereferencing: " + setObj);
400 setObj = ((JavaObjectNode)setObj).getSubObject();
401 }
402 setObj = Types.dereference(setObj);
403 if (matchingFields.size()>1)
404 {
405 throw new AspException("Too many fields match: " + ident);
406 }
407 Field field = (Field)matchingFields.get(0);
408 try {
409 if (DBG.isDebugEnabled())
410 DBG.debug("Setting field " + field +
411 " = " + setObj);
412 field.set(obj, setObj);
413 } catch (IllegalAccessException ex)
414 {
415 throw new AspException("Illegal Access Exception: " + ex.toString());
416 }
417 }
418
419 /**
420 * Obtains the value of a field.
421 * @return the value of the current field.
422 * @throws AspException if an error occurs
423 * @see SimpleReference#getValue
424 */
425 public Object getValue() throws AspException
426 {
427 if (DBG.isDebugEnabled())
428 DBG.debug("Getvalue of field " + ident +
429 " of object: " + obj);
430 if (matchingFields.size()<1)
431 {
432 Vector vec = new Vector();
433 Method met = findMethod(vec);
434 if (met != null) {
435 return executeMethod(met, vec);
436 }
437 throw new AspException("Unknown field name: " + ident);
438 }
439 if (matchingFields.size()>1)
440 {
441 throw new AspException("Too many fields match: " + ident);
442 }
443 Field field = (Field)matchingFields.get(0);
444 if (DBG.isDebugEnabled())
445 DBG.debug("One field matches: " + field);
446 try {
447 return Types.coerceToNode(field.get(obj));
448 } catch (IllegalAccessException ex)
449 {
450 throw new AspException("Illegal Access Exception: " + ex.toString());
451 }
452 }
453
454 /**
455 * Internal function which executes and dereferences
456 * the list of parameters in a VarListNode.
457 * @param params Parameters to execute and dereference
458 * @param context AspContext under which to execute the variables.
459 * @return The list of parameters executed and dereferenced.
460 * @throws AspException if an error occurs.
461 */
462 static private Vector executeAndDereference(VarListNode params,
463 AspContext context) throws AspException
464 {
465 Vector vec = (Vector)params.execute(context);
466 Vector deref = new Vector();
467 for (int i = 0; i < vec.size(); i++)
468 {
469 Object value = vec.get(i);
470 if (value instanceof JavaObjectNode) {
471 value = ((JavaObjectNode)value).
472 getSubObject();
473 }
474 deref.add(Types.dereference(value));
475 }
476 return deref;
477 }
478
479 /**
480 * Internal method which finds a method which matches the
481 * parameter list given.
482 * Uses the current field, method name.
483 * @param values Parameters for the method call.
484 * @return Method which matches the given parameters.
485 */
486 private Method findMethod(Vector values)
487 {
488 int curScore = 0;
489 Method curMethod = null;
490 if (DBG.isDebugEnabled()) DBG.debug("Number of methods: " +
491 matchingMethods.size());
492 for (int i = 0; i < matchingMethods.size(); i++)
493 {
494 Method method = (Method)matchingMethods.get(i);
495 Class params[] = method.getParameterTypes();
496 int score = parametersAreCompatible(params, values);
497 if (score > curScore)
498 {
499 curScore = score;
500 curMethod = method;
501 }
502 if (DBG.isDebugEnabled())
503 {
504 DBG.debug("Score: " + score);
505 DBG.debug("curScore: " + curScore);
506 DBG.debug("curMethod: " + curMethod);
507 }
508 }
509 return curMethod;
510 }
511
512 /**
513 * Internal method which executes a method with the given parameter
514 * list.
515 * @param met Method to execute
516 * @param parameter Parameters for the method.
517 * @return return value of method call
518 * @throws AspException if an error ocurrs.
519 */
520 private Object executeMethod(Method met, Vector params)
521 throws AspException
522 {
523 Class paramTypes[] = met.getParameterTypes();
524
525 Object varObjs[] = new Object[params.size()];
526 for (int i = 0; i < varObjs.length;i++)
527 {
528 if (paramTypes[i] == Integer.class ||
529 paramTypes[i] == int.class) {
530 varObjs[i] = Types.
531 coerceToInteger(params.get(i));
532 } else if (paramTypes[i] == Boolean.class ||
533 paramTypes[i] == boolean.class) {
534 varObjs[i] = Types.
535 coerceToBoolean(params.get(i));
536 } else if (paramTypes[i] == String.class) {
537 varObjs[i] = Types.
538 coerceToString(params.get(i));
539 } else if (paramTypes[i] == java.util.Date.class) {
540 Object parm = params.get(i);
541 if (parm instanceof UndefinedValueNode ||
542 parm instanceof NullNode) {
543 varObjs[i] = null;
544 } else {
545 varObjs[i] = Types.coerceToDate(parm).toDate();
546 }
547 } else if (paramTypes[i] == ByRefValue.class) {
548 ByRefValue newNode = new ByRefValue(params.get(i));
549 varObjs[i] = newNode;
550 } else {
551 Object parm = params.get(i);
552 if (parm instanceof UndefinedValueNode ||
553 parm instanceof NullNode) {
554 varObjs[i] = null;
555 } else if (parm instanceof PackedCharArrayNode) {
556 varObjs[i] = ((PackedCharArrayNode)params.get(i)).
557 internGetValues();
558 } else if (parm instanceof PackedByteArrayNode) {
559 varObjs[i] = ((PackedByteArrayNode)params.get(i)).
560 internGetValues();
561 } else if (parm instanceof ArrayNode) {
562 varObjs[i] = ((ArrayNode)params.get(i)).toJavaArray();
563 } else {
564 varObjs[i] = params.get(i);
565 }
566 }
567 }
568 try {
569 Object retVal = met.invoke(obj, varObjs);
570 if (DBG.isDebugEnabled())
571 DBG.debug("Return value = " + retVal);
572 for (int i = 0; i < params.size(); i++) {
573 if (paramTypes[i] == ByRefValue.class) {
574 params.set(i,((ByRefValue)varObjs[i]).getValue());
575 }
576 }
577 return Types.coerceToNode(retVal);
578 } catch (IllegalAccessException ex)
579 {
580 DBG.error("Error while calling Java object", ex);
581 throw new AspNestedException(ex);
582 } catch (InvocationTargetException ex)
583 {
584 Throwable targetEx = ex.getTargetException();
585 if (targetEx != null)
586 {
587 if (targetEx instanceof AspException) {
588 throw (AspException)targetEx;
589 }
590 }
591 DBG.error("Error while calling Java object", ex);
592 throw new AspNestedException(ex);
593 }
594 }
595
596 /**
597 * Internal method which tests how compatible a list of classes
598 * are to a parameters list.
599 * Scores are as follows:
600 * 2 = Exact match
601 * 1 = Coersion possible
602 * 0 = Coersion not possible.
603 * @param types Parameter types to test
604 * @param vars Parameter values
605 * @return score
606 */
607 static private int parametersAreCompatible(Class types[],
608 Vector vars)
609 {
610 int score = 0;
611 if (types.length != vars.size()) return 0;
612 /* Special case, no parameters */
613 if (types.length == 0) return 2;
614 for (int i = 0; i < types.length; i++)
615 {
616 Object value = vars.get(i);
617 Class valClass = null;
618
619 if (DBG.isDebugEnabled())
620 {
621 DBG.debug("Value: " + value);
622 DBG.debug("Value(class): " + value.getClass());
623 DBG.debug("Class: " + types[i]);
624 }
625
626 if (value instanceof ArrayNode) {
627 if (DBG.isDebugEnabled())
628 DBG.debug("Converted object to List");
629 value = ((ArrayNode)value).toJavaArray();
630 } else
631 if (value instanceof PackedCharArrayNode) {
632 if (DBG.isDebugEnabled())
633 DBG.debug("Converted object to char[]");
634 value = ((PackedCharArrayNode)value).internGetValues();
635 } else
636 if (value instanceof PackedByteArrayNode) {
637 if (DBG.isDebugEnabled())
638 DBG.debug("Converted object to byte[]");
639 value = ((PackedByteArrayNode)value).internGetValues();
640 }
641 if (value != null) valClass = value.getClass();
642
643 if (DBG.isDebugEnabled()) DBG.debug("valClass: " + valClass);
644
645 if ((value == null || valClass == NullNode.class
646 || valClass == UndefinedValueNode.class)
647 &&
648 Object.class.isAssignableFrom(types[i]))
649 {
650 score += 2;
651 } else
652 if (types[i] == valClass)
653 {
654 score += 2;
655 } else
656 if (types[i] == ByRefValue.class) {
657 score += 1;
658 } else
659 if ((types[i] == java.util.Date.class) &&
660 value instanceof AspDate) {
661 score += 2;
662 } else
663 if ((types[i] == AspDate.class) &&
664 value instanceof java.util.Date) {
665 score += 2;
666 } else
667 if ((types[i] == Boolean.class) ||
668 (types[i] == boolean.class)) {
669 /* XXX The score=2 case may be handled above */
670 if ((valClass == Boolean.class) ||
671 (valClass == boolean.class)) score += 2;
672 else
673 score += 1;
674 } else
675 if (types[i] == String.class) {
676 /* The exact case is handled above */
677 score += 1;
678 } else
679 if ((types[i] == int.class) ||
680 (types[i] == Integer.class) ||
681 (types[i] == long.class) ||
682 (types[i] == Long.class) ||
683 (types[i] == double.class) ||
684 (types[i] == Double.class) ||
685 (types[i] == float.class) ||
686 (types[i] == Float.class))
687 {
688 score += 1;
689 } else
690 if (types[i].isInstance(value))
691 {
692 score += 2;
693 }
694 if (DBG.isDebugEnabled())
695 DBG.debug("Cumulative score: " + score);
696 }
697 return score;
698 }
699
700 /**
701 * Internal method which obtains cached class information,
702 * class methods and fields.
703 * @param cls Class to obtain data of
704 * @return hashtable containin class data
705 */
706 static private Hashtable getClassData(Class cls)
707 {
708 Hashtable ht;
709 synchronized(classData)
710 {
711 ht = (Hashtable)classData.get(cls);
712 if (ht == null) {
713 ht = new Hashtable();
714 ht.put("allMethods", cls.getMethods());
715 ht.put("allFields", cls.getFields());
716 ht.put("methods", new Hashtable());
717 ht.put("fields", new Hashtable());
718 classData.put(cls, ht);
719 }
720 }
721 return ht;
722 }
723
724 /**
725 * Internal function which obtains the list of methods
726 * of a class which matches a given name (case insensitive).
727 * @param cls Class to obtain methods of
728 * @param ident Class name (case insensitive)
729 * @return vector of matching classes.
730 */
731 static synchronized Vector getMatchingMethods(Class cls, IdentNode ident)
732 {
733 Hashtable classData = getClassData(cls);
734 Hashtable cachedMethods =
735 (Hashtable)classData.get("methods");
736 if (!cachedMethods.containsKey(ident))
737 {
738 Vector matchingMethods = new Vector();
739
740 Method methods[] = (Method[])
741 classData.get("allMethods");
742 for (int i = 0; i < methods.length; i++)
743 {
744 if (methods[i].getName().equalsIgnoreCase(ident.ident))
745 {
746 matchingMethods.add(methods[i]);
747 }
748 }
749 cachedMethods.put(ident, matchingMethods);
750 return matchingMethods;
751 } else {
752 return (Vector)cachedMethods.get(ident);
753 }
754 }
755
756 /**
757 * Internal function which finds the list of fields which match
758 * a given name (case insensitive).
759 * @param cls Class to find fields of
760 * @param ident String name of field to find
761 * @return list of matching fields
762 */
763 static synchronized Vector getMatchingFields(Class cls, IdentNode ident)
764 {
765 Hashtable classData = getClassData(cls);
766 Hashtable cachedFields =
767 (Hashtable)classData.get("fields");
768 if (!cachedFields.containsKey(ident))
769 {
770 Vector matchingFields = new Vector();
771
772 Field fields[] = (Field[])
773 classData.get("allFields");
774 for (int i = 0; i < fields.length; i++)
775 {
776 if (fields[i].getName().equalsIgnoreCase(ident.ident))
777 {
778 matchingFields.add(fields[i]);
779 }
780 }
781 cachedFields.put(ident, matchingFields);
782 return matchingFields;
783 } else {
784 return (Vector)cachedFields.get(ident);
785 }
786 }
787 }
788 }