View Javadoc
1   package com.randomnoun.common;
2   
3   import java.io.IOException;
4   import java.io.Writer;
5   
6   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
7    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
8    */
9   
10  import java.lang.reflect.InvocationTargetException;
11  import java.lang.reflect.Method;
12  import java.util.ArrayList;
13  import java.util.Arrays;
14  import java.util.Collections;
15  import java.util.Comparator;
16  import java.util.Date;
17  import java.util.HashMap;
18  import java.util.Iterator;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.Set;
22  import java.util.concurrent.ConcurrentHashMap;
23  
24  import jakarta.servlet.http.HttpServletRequest;
25  
26  import org.apache.commons.beanutils.PropertyUtils;
27  
28  import com.randomnoun.common.io.StringBuilderWriter;
29  
30  /**
31   * Encoder/decoder of JavaBeans and 'structured' maps and lists. A structured
32   * map or list is any Map or List that satisfies certain conventions, listed below.
33   *
34   * <p>A structured map is any implementation of the {@link java.util.Map}
35   * interface, in which every key is a {@link String}, and every value is either a
36   * primitive wrapper type ({@link String}, {@link Long},
37   * {@link Integer}, etc...), a structured map or a structured list.
38   * A structured list is any implementation of the {@link java.util.List} interface,
39   * of which every value is a primitive wrapper type, a structured map or a structured list.
40   *
41   * <p>In this way, arbitrarily complex objects can be created and passed between
42   * Struts code and the business layer, or Struts code and the JSP layer,
43   * without resorting to the creation of application-specific
44   * datatypes. These datatypes are also used by the Spring JdbcTemplate framework to
45   * return values from a database, so it is useful to have some generic functions
46   * that operate on them.
47   *
48   * 
49   * @author knoxg
50   */
51  public class Struct {
52  
53      /** A ConcurrentReaderHashMap that maps Class objects to Maps, each of which
54       *  maps getter method names (e.g. getabcd) to Method objects. Method names should be
55       *  lower-cased when elements are added or looked up in this map, to provide
56       *  case-insensitivity.
57       */
58      private static Map<Class<?>, Map<String,Method>> gettersCache;
59   
60      /** A ConcurrentReaderHashMap that maps Class objects to Maps, each of which
61       *  maps setter method names (e.g. setabcd) to Method objects. Method names should be
62       *  lower-cased when elements are added or looked up in this map, to provide
63       *  case-insensitivity.
64       */
65      private static Map<Class<?>, Map<String,Method>> settersCache;
66  
67      /** Serialise Date objects using the Microsoft convention for Dates, 
68       * which is a String in the form <code>"/Date(millisSinceEpoch)/"</code>
69       */
70  	public static final String DATE_FORMAT_MICROSOFT = "microsoft";
71  	
72  	/** Serialise Date objects as milliseconds since the epoch */
73  	public static final String DATE_FORMAT_NUMERIC = "numeric";
74  
75  	/** This class can be serialised as a JSON value by calling it's toString() method */ 
76  	public static interface ToStringReturnsJson { }
77  	
78  	/** This class can be serialised as a JSON value by calling it's toJson() method */
79  	public static interface ToJson { 
80  		public String toJson(); 
81  	}
82  	
83  	/** This class can be serialised as a JSON value by calling it's toJson(String) method. 
84       * Multiple json formats are supported by supplying a jsonFormat string; e.g. 'simple'. 
85       * Passing null or an empty string should be equivalent to calling toJson() if the class also implements the Struct.ToJson interface.
86       */
87  	public static interface ToJsonFormat { 
88  		public String toJson(String jsonFormat); 
89  	}
90  	
91  	/** This class can be serialised as a JSON value by calling it's writeJsonFormat() method 
92       * Multiple json formats are supported by supplying a jsonFormat string; e.g. 'simple'. 
93       * Passing null or an empty string should be equivalent to calling toJson() if the class also implements the Struct.ToJson interface.
94  	 */
95  	public static interface WriteJsonFormat { 
96  		public void writeJsonFormat(Writer w, String jsonFormat) throws IOException; 
97  	}
98  	
99  	// @TODO when we move to jdk 11 layer extend these interfaces over each other with default implementations, + filteredJson interface
100 	
101 	/** A comparator that allows elements within lists to be sorted, allowing
102 	 * nulls (in order to sort map keys)
103 	 */
104     static class ListComparator implements Comparator
105     {
106         public int compare(Object o1, Object o2) {
107             if (o1 == null && o2 == null) {
108                 return 0;
109             } else if (o1 == null) {
110                 return -1;
111             } else if (o2 == null) {
112                 return 1;
113             }
114             return ((Comparable)o1).compareTo(o2);
115         }
116     }
117     
118     /** Backwards compatible class until this is removed */
119     public static class ListContainingNullComparator extends ListComparator { };
120     
121     
122     /** A Comparator which performs comparisons between two rows in a structured list (used
123      *  in sorting). This comparator is equivalent to an 'ORDER BY' on a single field
124      *  returned by the Spring JdbcTemplate method.
125      */
126     public static class StructuredListComparator implements Comparator {
127         /** The key field to sort on */
128         private String fieldName;
129 
130         /** Create a new StructuredListComparator object, which will sort on the
131          *  supplied field.
132          *
133          * @param fieldName The name of the field to sort on
134          */
135         public StructuredListComparator(String fieldName) {
136             this.fieldName = fieldName;
137         }
138 
139         /** Compare two structured list elements
140          *
141          * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
142          */
143         public int compare(Object a, Object b)
144             throws IllegalArgumentException {
145             if (!(a instanceof Map)) {
146                 throw new IllegalArgumentException("List must be composed of Maps");
147             }
148             if (!(b instanceof Map)) {
149                 throw new IllegalArgumentException("List must be composed of Maps");
150             }
151 
152             Map mapA = (Map) a;
153             Map mapB = (Map) b;
154             if (!mapA.containsKey(fieldName)) {
155                 throw new IllegalArgumentException("keyField '" + fieldName + "' not found in Map");
156             }
157             if (!mapB.containsKey(fieldName)) {
158                 throw new IllegalArgumentException("keyField '" + fieldName + "' not found in Map");
159             }
160 
161             Object objectA = mapA.get(fieldName);
162             if (!(objectA instanceof Comparable)) {
163                 throw new IllegalArgumentException("keyField '" + fieldName + "' element must implement Comparable");
164             }
165             return ((Comparable) objectA).compareTo(mapB.get(fieldName));
166         }
167     }
168 
169 
170     /** A Comparator which performs comparisons between two rows in a structured list (used
171      *  in sorting), keyed on a case-insensitive String value. This comparator is similar to an 
172      * 'ORDER BY' on a single field returned by the Spring JdbcTemplate method.
173      */
174     public static class StructuredListComparatorIgnoreCase implements Comparator {
175 
176         /** The key field to sort on */
177         private String fieldName;
178 
179         /** Create a new StructuredListComparatorIgnoreCase object, which will sort on the
180          *  supplied field.
181          *
182          * @param fieldName The name of the field to sort on
183          */
184         public StructuredListComparatorIgnoreCase(String fieldName) {
185             this.fieldName = fieldName;
186         }
187 
188         /** Compare two structured list elements
189          *
190          * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
191          */
192         public int compare(Object a, Object b)
193             throws IllegalArgumentException {
194             if (!(a instanceof Map)) {
195                 throw new IllegalArgumentException("List must be composed of Maps");
196             }
197             if (!(b instanceof Map)) {
198                 throw new IllegalArgumentException("List must be composed of Maps");
199             }
200 
201             Map mapA = (Map) a;
202             Map mapB = (Map) b;
203             if (!mapA.containsKey(fieldName)) {
204                 throw new IllegalArgumentException("keyField '" + fieldName + "' not found in Map");
205             }
206             if (!mapB.containsKey(fieldName)) {
207                 throw new IllegalArgumentException("keyField '" + fieldName + "' not found in Map");
208             }
209 
210             String stringA = (String) mapA.get(fieldName);
211             return stringA.compareToIgnoreCase((String) mapB.get(fieldName));
212         }
213     }
214 
215 
216     /** Sorts a structured list on the supplied key field
217      *
218      * @param list The list to search.
219      * @param keyField The name of the key field.
220      *
221      * @throws NullPointerException if list or keyField is set to null.
222      * @throws IllegalStateException if the list is not composed of Maps
223      */
224     static public void sortStructuredList(List list, String keyField) {
225         if (list == null) { throw new NullPointerException("Cannot search null list"); }
226         if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
227 
228         Comparator comparator = new StructuredListComparator(keyField);
229         Collections.sort(list, comparator);
230     }
231 
232     /** Sorts a structured list on the supplied key field; the key value is sorted
233      * case-insensitively (it must be of type String or a ClassCastException will occur).
234      *
235      * @param list The list to search.
236      * @param keyField The name of the key field.
237      *
238      * @throws NullPointerException if list or keyField is set to null.
239      * @throws IllegalStateException if the list is not composed of Maps
240      */
241     static public void sortStructuredListIgnoreCase(List list, String keyField) {
242         if (list == null) { throw new NullPointerException("Cannot search null list"); }
243         if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
244 
245         Comparator comparator = new StructuredListComparatorIgnoreCase(keyField);
246         Collections.sort(list, comparator);
247     }
248 
249 
250     /** Set all fields in object 'obj' using values in request. Each request parameter
251      *  is checked against the object to see if a bean setter method exists for that
252      *  parameter name. If it does, then it is invoked, using the parameter value
253      *  as the setter argument. This method uses the
254      * {@link Struct#setValue(Object, String, Object, boolean, boolean, boolean)} method
255      *  to dynamically create map and list elements as required if the object being
256      *  set implements the Map interface.
257      *
258      *  <p>e.g. if passed a Map object, and the HttpServletRequest has a parameter named
259      *  "<code>table[12].id</code>" with the value "<code>1234</code>", then:
260      *  <ul><li>a List object named "table" is created
261      *  in the Map,
262      *  <li>the list is increased to allow at least 12 elements,
263      *  <li> that element is set to a new Map object ...
264      *  <li> ... which contains the String key "id", which is assigned the  value "1234".
265      *  </ul>
266      *  <p>This processing allows developers to pass arbitrarily complex structures through
267      *  from HttpServletRequest name/value pairs.
268      *
269      *  <p>This method operates on both structured objects (Maps/Lists) or 
270      *  data-transfer objects (DTOs). For example, if passed a bean-like Object which had a getTable() method that
271      *  returned a List, then this would be used to perform the processing above. (If the
272      *  List returned a Map at index 12, then an 'id' key would be created as before; if
273      *  the List returned another Object at index 12, then the setId() method would be
274      *  called instead; if the list returned null at index 12, then a Map would be created as before).
275      *
276      *  <p>NB: If an object is not a Map, and does not have the appropriate setter() method,
277      *  then this function will *not* raise an exception. This is intentional behaviour -
278      *  (use {@link #setValue(Object, String, Object, boolean, boolean, boolean)} if you
279      *  need more fine-grained control.
280      *
281      *  <p>You can limit the setters that are invoked by using the
282      *  {@link #setFromRequest(Object, HttpServletRequest, String[])} form of this method.
283      *
284      * @param obj The object being set.
285      * @param request The request containing the source data
286      *
287      * @throws RuntimeException if an invocation target exception occurred whilst
288      *   calling the object's set() methods.
289      */
290     public static void setFromRequest(Object obj, HttpServletRequest request) {
291         setFromRequest(obj, request, null);
292     }
293 
294     /** Set all fields in object 'obj' using values in request. Only the request
295      *  parameter names passed in through the 'fields' argument will be used
296      *  to populate the object. If the named parameter does not exist in the request,
297      *  then the setter for that parameter is not invoked.
298      *
299      *  <p>See {@link #setFromRequest(Object, HttpServletRequest)} for more information
300      *  on how this method operates.
301      *
302      * @param obj The object being set.
303      * @param request The request containing the source data
304      * @param fields An array of strings, denoting the fields that are sourced from
305      *   the request. All other parameters in the request are ignored.
306      *
307      * @throws RuntimeException if an invocation target exception occurred whilst
308      *   calling the object's set() methods.
309      */
310     public static void setFromRequest(Object obj, HttpServletRequest request, String[] fields) {
311         Iterator paramListIter;
312 
313         if (fields == null) {
314             paramListIter = request.getParameterMap().keySet().iterator();
315         } else {
316             paramListIter = Arrays.asList(fields).iterator();
317         }
318 
319         while (paramListIter.hasNext()) {
320             String parameter = (String) paramListIter.next();
321             if (parameter.endsWith("[]")) {
322             	parameter = parameter.substring(0, parameter.length()-2);
323             	String values[] = request.getParameterValues(parameter);
324             	if (values!=null) {
325 	            	for (int i=0; i<values.length; i++) {
326 	            		values[i] = Text.replaceString(values[i], "\015\012", "\n");
327 	            	}
328             	}
329             	setValue(obj, parameter, values, true, true, true);
330             } else {
331 	            String value = request.getParameter(parameter);
332 	            
333 	            // convert CR-LFs into java newlines 
334 	            value = Text.replaceString(value, "\015\012", "\n");
335 	
336 	            // The null check below is commented out because we need to be able to pass
337 	            // null values through in order to set booleans values for checkboxes
338 	            // that are unchecked (these are represented in HTTP by missing (null) parameters)
339 	
340 	            //if (value != null) { 
341 	            setValue(obj, parameter, value, true, true, true);
342 	            //}
343             }
344         }
345     }
346 
347 
348     /** Similar to the {@link #setFromRequest(Object, HttpServletRequest)} method, but
349      *  uses a Map instead of a request.
350      * 
351      * @param obj The object being set.
352      * @param map The Map containing the source data
353      * @param ignoreMissingSetter passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)} 
354      * @param convertStrings passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)}
355      * @param createMissingElements passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)}
356      */
357     public static void setFromMap(Object obj, Map map, 
358       boolean ignoreMissingSetter, boolean convertStrings, boolean createMissingElements) 
359     {
360         setFromMap(obj, map, ignoreMissingSetter, convertStrings, createMissingElements, null); 
361     }
362 
363     /** Similar to the {@link #setFromRequest(Object, HttpServletRequest, String[])} method, but
364      *  uses a Map instead of a request.
365      * 
366      * @param obj The object being set.
367      * @param map The Map containing the source data
368      * @param ignoreMissingSetter passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)} 
369      * @param convertStrings passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)}
370      * @param createMissingElements passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)}
371      * @param fields An array of strings, denoting the fields that are sourced from
372      *   the request. All other parameters in the request are ignored.
373      */
374     public static void setFromMap(Object obj, Map map, 
375       boolean ignoreMissingSetter, boolean convertStrings, boolean createMissingElements, 
376       String[] fields) 
377     {
378         if (fields == null) {
379             for (Iterator<?> i = map.entrySet().iterator(); i.hasNext(); ) {
380                 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
381                 setValue(obj, (String) entry.getKey(), entry.getValue(), ignoreMissingSetter, convertStrings, createMissingElements);
382             }
383         } else {
384             for (int i = 0; i<fields.length; i++) {
385                 setValue(obj, fields[i], map.get(fields[i]), ignoreMissingSetter, convertStrings, createMissingElements);
386             }
387         }
388     }
389 
390     /** Similar to the {@link #setFromRequest(Object, HttpServletRequest, String[])} method, but
391      *  uses a Map instead of a request.
392      * 
393      * @param targetObj The object being set.
394      * @param sourceObj The Map containing the source data
395      * @param ignoreMissingSetter passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)} 
396      * @param convertStrings passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)}
397      * @param createMissingElements passed to {@link #setValue(Object, String, Object, boolean, boolean, boolean)}
398      * @param fields An array of strings, denoting the fields that are sourced from
399      *   the request. All other parameters in the request are ignored.
400      */
401     public static void setFromObject(Object targetObj, Object sourceObj, 
402       boolean ignoreMissingSetter, boolean convertStrings, boolean createMissingElements, 
403       String[] fields) 
404     {
405     	// probably something in bean-utils for all of this
406         if (fields == null) {
407         	/*
408             for (Iterator i = sourceObj.entrySet().iterator(); i.hasNext(); ) {
409                 Map.Entry entry = (Map.Entry) i.next();
410                 setValue(targetObj, (String) entry.getKey(), entry.getValue(), ignoreMissingSetter, convertStrings, createMissingElements);
411             }
412             */
413         	throw new UnsupportedOperationException("null field list not supported");
414         } else {
415             for (int i = 0; i<fields.length; i++) {
416                 setValue(targetObj, fields[i], getValue(sourceObj, fields[i]), ignoreMissingSetter, convertStrings, createMissingElements);
417             }
418         }
419     }
420     
421     
422 
423     /** Return a getter Method for a class. 
424      *
425      * <p>(Getters are cached by this class)
426      *
427      * @param clazz The class we are retrieving the getter Method for
428      * @param propertyName The name of the property on this class; e.g. "asd" will return the
429      *   method "getAsd()"
430      * 
431      * @return the getter Method for the supplied class.
432      */
433     private static Method getGetterMethod(Class<?> clazz, String propertyName) {
434         Map<String,Method> getters = (Map<String,Method>) gettersCache.get(clazz);
435 
436         if (getters == null) {
437             getters = new HashMap<String,Method>();
438 
439             Method[] methods = clazz.getMethods();
440             Method method;
441 
442             // get a list of setters this object contains
443             for (int i = 0; i < methods.length; i++) {
444                 method = methods[i];
445 
446                 if (method.getName().startsWith("get") && method.getParameterTypes().length == 1) {
447                     String property = method.getName().substring(3);
448                     getters.put(property.toLowerCase(), method);
449                 }
450             }
451             gettersCache.put(clazz, getters);
452         }
453 
454         return (Method) getters.get(propertyName.toLowerCase());
455     }
456 
457     /** Return a setter method for a class. Note that if a class supplies multiple setters (with
458      * one parameter each), then one will be arbitrarily chosen. So don't have multiple setters ! 
459      *
460      * <p>(Setters are cached by this class)
461      *
462      * @param clazz The class we are retrieving the setter Method for
463      * @param propertyName The name of the property on this class; e.g. "asd" will return the
464      *   first "setAsd(...)" found on this class with one parameter.
465      * 
466      * @return The setter Method for the supplied class
467      */
468     private static Method getSetterMethod(Class<?> clazz, String propertyName) {
469         Map<String,Method> setters = (Map<String, Method>) settersCache.get(clazz);
470 
471         if (setters == null) {
472             setters = new HashMap<String,Method>();
473 
474             Method[] methods = clazz.getMethods();
475             Method method;
476 
477             // get a list of setters this object contains
478             for (int i = 0; i < methods.length; i++) {
479                 method = methods[i];
480 
481                 if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) {
482                     String property = method.getName().substring(3);
483                     setters.put(property.toLowerCase(), method);
484                 }
485             }
486             settersCache.put(clazz, setters);
487         }
488 
489         return (Method) setters.get(propertyName.toLowerCase());
490     }
491 
492     /** Call setter method, converting from String */
493     private static void callSetterMethodWithString(Object target, Method setter, String value) {
494         Class<?> clazz = setter.getParameterTypes()[0];
495         Object realValue;
496 
497         if (clazz.equals(String.class)) {
498             realValue = value;
499         } else if (clazz.equals(boolean.class)) {
500             realValue = Boolean.valueOf(value);
501         } else if (clazz.equals(long.class)) {
502         	realValue = value.equals("") ? new Long(0) : new Long(value);
503         } else if (clazz.equals(int.class)) {
504         	realValue = value.equals("") ? new Integer(0) : new Integer(value);
505         } else if (clazz.equals(double.class)) {
506         	realValue = value.equals("") ? new Double(0) : new Double(value);
507         } else if (clazz.equals(float.class)) {
508         	realValue = value.equals("") ? new Float(0) : new Float(value);
509         } else if (clazz.equals(short.class)) {
510         	realValue = value.equals("") ? new Short((short) 0) : new Short(value);
511         // missing char.class here
512 
513         } else if (clazz.equals(Long.class)) {
514         	realValue = value.equals("") ? null : new Long(value);
515         } else if (clazz.equals(Integer.class)) {
516         	realValue = value.equals("") ? null : new Integer(value);
517         } else if (clazz.equals(Double.class)) {
518         	realValue = value.equals("") ? null : new Double(value);
519         } else if (clazz.equals(Float.class)) {
520         	realValue = value.equals("") ? null : new Float(value);
521         } else if (clazz.equals(Short.class)) {
522         	realValue = value.equals("") ? null : new Short(value);
523         // missing Character.class here
524         
525         
526         } else {
527             throw new IllegalArgumentException("Cannot convert string value to " + "'" + clazz.getName() + "' required for setter method '" + setter.getName() + "'");
528         }
529 
530         callSetterMethod(target, setter, realValue);
531     }
532 
533     /** Call getter method */
534     private static Object callGetterMethod(Object target, Method getter) {
535         Object[] methodArgs = new Object[0];
536 
537         try {
538             return getter.invoke(target, methodArgs);
539         } catch (InvocationTargetException ite) {
540             // if (ite instanceof RuntimeException) { throw ite; }
541             throw (IllegalArgumentException) new IllegalArgumentException("Exception calling method '" + getter.getName() + "' on object '" + target.getClass().getName() + "': " + ite.getMessage()).initCause(ite);
542         } catch (IllegalAccessException iae) {
543             throw (IllegalArgumentException) new IllegalArgumentException("Exception calling method '" + getter.getName() + "' on object '" + target.getClass().getName() + "': " + iae.getMessage()).initCause(iae);
544         }
545     }
546 
547     /** Checks whether the an instance of the value class can be assigned to a variable
548      * of 'variableClass' class via a
549      * {@see java.lang.reflect.Method#invoke(java.lang.Object, java.lang.Object[])}
550      * method. This is different to the normal
551      * {@see java.lang.Class#isAssignableFrom(java.lang.Class)} method since it
552      * also takes primitive to wrapper conversions into account.
553      *
554      * @param variableClass The class representing the variable we are assigning to
555      *   (e.g. the 'x' in 'x = y')
556      * @param value The class representing the value we are assigning to the variable
557      *   (e.g. the 'y' in 'x = y')
558      *
559      * @return true if the assignment is compatible, false otherwise.
560      */
561     private static boolean isAssignmentSafe(Class variableClass, Class value) {
562         // check for instance or wrapper type
563         return variableClass.isAssignableFrom(value) || 
564           (variableClass.equals(boolean.class) && value.equals(Boolean.class)) || 
565           (variableClass.equals(char.class) && value.equals(Character.class)) || 
566           (variableClass.equals(byte.class) && value.equals(Byte.class)) || 
567           (variableClass.equals(short.class) && value.equals(Short.class)) || 
568           (variableClass.equals(int.class) && value.equals(Integer.class)) || 
569           (variableClass.equals(long.class) && value.equals(Long.class)) || 
570           (variableClass.equals(float.class) && value.equals(Float.class)) || 
571           (variableClass.equals(double.class) && value.equals(Double.class));
572     }
573 
574     /** Call setter method */
575     private static void callSetterMethod(Object target, Method setter, Object value) {
576 
577         Object[] methodArgs = new Object[1];
578         methodArgs[0] = value;
579         
580         if (value!=null) {
581 	        Class<?> setterParamClass = setter.getParameterTypes()[0];
582 	        Class<?> valueClass = value.getClass();
583 	        
584 	        // special case for assigning Numbers (including BigDecimals) to longs
585 			if (setterParamClass.equals(Long.class) && Number.class.isAssignableFrom(valueClass)) { // valueClass.equals(BigDecimal.class)
586 				methodArgs[0] = new Long(((Number)value).longValue()); 
587 			} else if (setterParamClass.equals(long.class) && Number.class.isAssignableFrom(valueClass)) { // valueClass.equals(BigDecimal.class)
588 	        	methodArgs[0] = new Long(((Number)value).longValue()); 
589 	        } else {
590 		        if (!isAssignmentSafe(setterParamClass, value.getClass())) {
591 		        	// see if we can shoe-horn it in
592 		        	
593 		        	
594 		            throw new IllegalArgumentException("Exception calling method '" + setter.getName() + 
595 	  				  "' on object '" + target.getClass().getName() + "': value object of type '" + 
596 	  				  valueClass.getName() + "' is not assignment-compatible with setter parameter '" + 
597 	  				  setterParamClass.getName() + "'");
598 		        }
599 	        }
600         }
601 
602         try {
603             setter.invoke(target, methodArgs);
604         } catch (InvocationTargetException ite) {
605             // if (ite instanceof RuntimeException) { throw ite; }
606             throw (IllegalArgumentException) new IllegalArgumentException("Exception calling method '" + setter.getName() + "' on object '" + target.getClass().getName() + "': " + ite.getMessage()).initCause(ite);
607         } catch (IllegalAccessException iae) {
608             throw (IllegalArgumentException) new IllegalArgumentException("Exception calling method '" + setter.getName() + "' on object '" + target.getClass().getName() + "': " + iae.getMessage()).initCause(iae);
609         } catch (Exception e) {
610             // if (ite instanceof RuntimeException) { throw ite; }
611             throw (IllegalArgumentException) new IllegalArgumentException("Exception calling method '" + setter.getName() + "' on object '" + target.getClass().getName() + "': " + e.getMessage()).initCause(e);
612         }
613     }
614 
615 
616     /** Return a single value from a structured map. Returns null if the
617      *  value does not exist, or if an error occurred retrieving it.
618      *
619      * @param object The structure map
620      * @param key The key of the value we wish to retrieve (e.g. "abc.def[12].ghi")
621      *
622      * @return The value
623      */
624     static public Object getValue(Object object, String key) {
625         // @TODO (low priority) make this function consistent with setValue()
626         // the PropertyUtils object below is taken from Jakarta's commons-beanutils.jar 
627         // package, which has similar, but not identical semantics. 
628         // The provided functionality should hopefully be a superset of that
629         // provided by this class anyway, so it may not be important. 
630         // (beanutils property retrieval allows curly-brace "{"/"}"-style syntax
631         // which we don't allow here).
632 
633         try {
634             return PropertyUtils.getProperty(object, key);
635         } catch (Exception e) {
636             return null;
637         }
638     }
639 
640     /** Sets a single value in a structured object. The object may be composed
641      * of javabean-style objects, Map/List classes, or a combination of the two.
642      * 
643      * <p>There is a possible denial-of-service attack that can be used against
644      * this method which you should probably be aware of, but can't do anything about:
645      * if a large list index is supplied, e.g. "<code>abc[1293921]</code>", then a list will be
646      * generated with this many elements in it. There are workarounds for this, 
647      * but none are really satisfactory.
648      * 
649      * <p>It should also be noted that even if an exception is thrown within this
650      * method, the data structure underneath it may still have been modified, e.g.
651      * setting "<code>abc.def.ghi</code>" may successfully construct 'def' but later
652      * fail on 'ghi'. 
653      * 
654      * @param object The structure map or list
655      * @param key The key of the value we wish to set (e.g. "abc.def[12].ghi"). Note
656      *   that map keys are assumed; i.e. they are not enclosed in 'curly braces'.
657      * @param value The value to set this to
658      * @param ignoreMissingSetter If we are asked to set a value in an object which
659      *   does not have an appropriate setter, don't raise an exception (just does
660      *   nothing instead).
661      * @param convertStrings If we are asked to set a string value in an object
662      *   which has a setter, but of a different type (e.g. setCustomerId(Long)),
663      *   performs conversions to that type automatically. (Useful when setting
664      *   values sourced from a HttpServletRequest, which will always be of String type).
665      * @param createMissingElements If we are asked to set a value in a Map or List
666      *   that does not exist, will create the required Map keys or extend the List
667      *   so that the value can be set.
668      *
669      * @throws IllegalArgumentException if
670      * <ul><li>An null map or list is navigated down
671      * <li>A non-List object is invoked with the '[]' operator
672      * <li>A non-Map object is invoked with the '.' operator, or the object
673      *   has no getter/setter method with the appropriate name
674      * <li>An empty mapped property is supplied e.g. bob..something
675      * <li>An invalid key was supplied (e.g. 'bob[123' -- no closing square bracket)
676      * </ul>
677      * @throws NumberFormatException if an array index cannot be converted to
678      *   an integer via {@link java.lang.Integer#parseInt(java.lang.String)}.
679      */
680     static public void setValue(Object object, String key, Object value, boolean ignoreMissingSetter, boolean convertStrings, boolean createMissingElements)
681         throws NumberFormatException 
682     {
683         if (object == null) { throw new NullPointerException("null object"); }
684         if (key == null) { throw new NullPointerException("null key"); }
685 
686         // parse state: 
687         //   0=searching for new value (at start of line) 
688         //   1=consuming list index (after '[')
689         //   2=consuming map index (after start of line or after '.')
690         //   3=search for new value (after ']')
691         Object ref = object; // reference into object
692 
693         int parseState = 0;
694         int length = key.length();
695         String element;
696         int elementInt = -1;
697         List lastList = null;
698         String parentName = null;
699         Object parentRef = null;
700         
701         if (object instanceof List) { lastList = (List) object; }
702 
703         char ch;
704         StringBuffer buffer = new StringBuffer();
705 
706         for (int pos = 0; pos < length; pos++) {
707             ch = key.charAt(pos);
708 
709             // System.out.println("pos " + pos + ", state=" + parseState + ", nextchar=" + ch + ", buf=" + buffer);
710             switch (parseState) {
711                 
712                 case 0: 
713                     if (ch == '[') {
714                         buffer.setLength(0);
715                         parseState = 1;
716                     } else { // could check for legal map index characters here
717                         buffer.setLength(0);
718                         buffer.append(ch);
719                         parseState = 2;
720                     }
721                     break;
722                     
723                 case 3:
724                     if (ch == '[') {
725                         buffer.setLength(0);
726                         parseState = 1;
727                     } else if (ch == '.') {
728                         buffer.setLength(0);
729                         parseState = 2;
730                     } else {
731                         throw new IllegalArgumentException("Expecting '[' or '.' after ']' in key '" + key + "'; found '" + ch + "'");
732                     }
733                     break;
734                 
735                 case 1:
736                     if (ch >= '0' && ch <= '9') {
737                         buffer.append(ch);
738                     } else if (ch == ']') {
739                         element = buffer.toString();
740                         elementInt = Integer.parseInt(element);
741 
742                         if (ref == null) {
743                             // create list if parent was a map
744                             if ((parentRef instanceof Map) && createMissingElements) {
745                                 ref = new ArrayList();
746                                 ((Map) parentRef).put(parentName, ref);
747                             } else if ((parentRef instanceof List) && createMissingElements) {
748                                 ref = new ArrayList();
749                                 ((List) parentRef).set(Integer.parseInt(parentName), ref);
750                             } else {
751                                 throw new IllegalArgumentException("Could not retrieve indexed property " + element + " in key '" + key + "' from null object");
752                             }
753                         }
754 
755                         if (ref instanceof List) {
756                             lastList = (List) ref;
757 
758                             // XXX: denial of service attacks possible here
759                             // (large list number may exceed available memory)
760                             if (lastList.size() <= elementInt) {
761                                 setListElement(lastList, elementInt, null);
762                             }
763 
764                             parentName = element;
765                             parentRef = ref;
766                             ref = lastList.get(elementInt);
767                         } else {
768                             // could attempt to reflect on a get(int)-style method,
769                             // but this seems like overkill; anything that has List-like
770                             // semantics should implement the List interface.
771                             throw new IllegalArgumentException("Could not retrieve indexed property " + element + " in key '" + key + "' from object of type '" + ref.getClass().getName() + "'");
772                         }
773 
774                         parseState = 3;
775                     } else {
776                         throw new IllegalArgumentException("Illegal character '" + ch + "'" + " found in list index");
777                     }
778                     break;
779                 
780                 case 2:
781                     if (ch != '.' && ch != '[') {
782                         buffer.append(ch);
783                     } else {
784                         // navigate map
785                         element = buffer.toString();
786 
787                         if (ref == null) {
788                             if ((parentRef instanceof List) && createMissingElements) {
789                                 ref = new HashMap();
790                                 ((List) parentRef).set(Integer.parseInt(parentName), ref);
791                             } else if ((parentRef instanceof Map) && createMissingElements) {
792                                 ref = new HashMap();
793                                 ((Map) parentRef).put(parentName, ref);
794                             } else {
795                                 throw new IllegalArgumentException("Could not retrieve mapped property '" + element + "' in key '" + key + "' from null object");
796                             }
797                         }
798 
799                         if (element.equals("")) {
800                             throw new IllegalArgumentException("Could not retrieve empty mapped property '" + element + "' in key '" + key + "'");
801                         } else if (ref instanceof Map) {
802                             parentName = element;
803                             parentRef = ref;
804                             ref = ((Map) ref).get(element);
805                         } else {
806                             // look for getter method
807                             Method getter = getGetterMethod(ref.getClass(), element);
808 
809                             if (getter == null) {
810                                 throw new IllegalArgumentException("Could not retrieve mapped property '" + element + "' in key '" + key + "' for class '" + ref.getClass().getName() + "': no getter method found");
811                             }
812 
813                             parentName = null; // can't dynamically create these
814                             parentRef = null;
815                             ref = callGetterMethod(ref, getter);
816                         }
817 
818                         buffer.setLength(0);
819 
820                         if (ch == '[') {
821                             parseState = 1;
822                         } else if (ch == '.') {
823                             parseState = 2;
824                         } else {
825                             throw new IllegalStateException("Unexpected character '" + ch + "' parsing key '" + key + "'");
826                         }
827                     }
828                     break;
829                 
830                 default:
831                     throw new IllegalStateException("Unexpected state " + parseState + " parsing key '" + key + "'");
832             }
833         }
834 
835         if (parseState == 0) {
836             throw new IllegalStateException("Unexpected error after parsing key '" + key + "'");
837         } else if (parseState == 1) {
838             throw new IllegalStateException("Missing ']' in key '" + key + "'");
839         } else if (parseState == 2) {
840             // set mapped property
841             element = buffer.toString();
842 
843             if (ref == null) {
844                 if ((parentRef instanceof Map) && createMissingElements) {
845                     ref = new HashMap();
846                     ((Map) parentRef).put(parentName, ref);
847                 } else if ((parentRef instanceof List) && createMissingElements) {
848                     ref = new HashMap();
849                     ((List) parentRef).set(Integer.parseInt(parentName), ref);
850                 } else {
851                     throw new IllegalArgumentException("Could not set mapped property '" + element + "' in key '" + key + "' for null object");
852                 }
853             }
854 
855             if (element.equals("")) {
856                 throw new IllegalArgumentException("Could not set empty mapped property '" + element + "' in key '" + key + "'");
857             } else if (ref instanceof Map) {
858                 ((Map) ref).put(element, value);
859             } else {
860                 Method setter = getSetterMethod(ref.getClass(), element);
861 
862                 if (setter == null) {
863                     // System.out.println("Missing setter '" + element + "'");
864                     if (!ignoreMissingSetter) {
865                         throw new IllegalArgumentException("Could not set mapped property '" + element + "' in key '" + key + "' for class '" + ref.getClass().getName() + "': no setter method found");
866                     }
867                 } else {
868                     // System.out.println("Setting '" + element + "' with value " + value);
869                     try {
870 	                    if (convertStrings && ((value == null) || (value instanceof String))) {
871 	                        if (value==null) { value = ""; }
872 	                        callSetterMethodWithString(ref, setter, (String) value);
873 	                    } else {
874 	                        callSetterMethod(ref, setter, value);
875 	                    }
876                     } catch (Exception e) {
877                     	throw (IllegalArgumentException) new IllegalArgumentException("Could not set field '" + key + "' with value '" + value + "'").initCause(e);
878                     }
879                 }
880             }
881         } else if (parseState == 3) {
882             // set list property
883             // ref currently points to the current list element; we want to set the 
884             // value of the last list we saw, contained in lastList.
885             ref = lastList;
886 
887             if (ref == null) {
888                 throw new IllegalArgumentException("Could not set indexed property '" + elementInt + "' in key '" + key + "' for null object");
889             }
890 
891             if (ref instanceof List) {
892                 ((List) ref).set(elementInt, value);
893             } else {
894                 throw new IllegalArgumentException("Could not set indexed property " + elementInt + " in key '" + key + "' in object of type '" + ref.getClass().getName() + "'");
895             }
896         }
897     }
898 
899     /** Sets the element at a particular list index to a particular object.
900      * This differs from the standard List.set() method inasmuch as the list
901      * is allowed to grow in the case where index&gt;=List.size(). If the list
902      * needs to grow to accomodate the new element, then null objects are
903      * appended until the list is large enough.
904      *
905      * @param list   The list to modify
906      * @param index  The position within the list we wish to set to this object
907      * @param object The object to be placed into the list.
908      */
909     public static void setListElement(List list, int index, Object object) {
910         int size;
911 
912         if (list == null) {
913             throw new NullPointerException("list parameter must not be null");
914         }
915 
916         if (index < 0) {
917             throw new IndexOutOfBoundsException("index parameter must be >= 0");
918         }
919 
920         size = list.size();
921 
922         while (index >= size) {
923             list.add(null);
924             size = size + 1;
925         }
926 
927         list.set(index, object);
928     }
929 
930     /** As per {@link #getStructuredListItem(List, String, Object)}, with a 
931      * numeric key.
932      * 
933      * @param list The list to search.
934      * @param keyField The name of the key field.
935      * @param longValue The key value to search for
936      *
937      * @return The requested element in the list, or null if the element cannot be found.
938      *
939      * @throws NullPointerException if list or keyField is set to null.
940      * @throws IllegalStateException if the list is not composed of Maps
941      */
942     static public Map getStructuredListItem(List list, String keyField, long longValue) {
943         if (list == null) { throw new NullPointerException("Cannot search null list"); }
944         if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
945 
946         Map row;
947         Object foundKey;
948         for (Iterator i = list.iterator(); i.hasNext();) {
949             try {
950                 row = (Map) i.next();
951             } catch (ClassCastException cce) {
952                 throw (IllegalStateException) new IllegalStateException("List must be composed of Maps").initCause(cce);
953             }
954             foundKey = row.get(keyField);
955             if (foundKey != null) {
956                 if (!(foundKey instanceof Number)) {
957                     throw (IllegalStateException) new IllegalStateException("Key is not numeric (found '" + foundKey.getClass().getName() + "' instead)");
958                 }
959                 if (((Number) foundKey).longValue() == longValue) {
960                     return row;
961                 }
962             }
963         }
964         return null;
965     }
966 
967     /** As per {@link #getStructuredListObject(List, String, Object)}, with a 
968      * numeric key.
969      *
970      * @param list The list to search.
971      * @param keyField The name of the key field.
972      * @param longValue The key value to search for
973      *
974      * @return The requested element in the list, or null if the element cannot be found.
975      *
976      * @throws NullPointerException if list or keyField is set to null.
977      * @throws IllegalStateException if the list is not composed of Maps
978      */
979     static public Object getStructuredListObject(List list, String keyField, long longValue) {
980         if (list == null) { throw new NullPointerException("Cannot search null list"); }
981         if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
982         Object row;
983         Object foundKey;
984         for (Iterator i = list.iterator(); i.hasNext();) {
985             row = i.next();
986             foundKey = Struct.getValue(row, keyField); 
987             if (foundKey != null) {
988                 if (!(foundKey instanceof Number)) {
989                     throw (IllegalStateException) new IllegalStateException("Key is not numeric (found '" + foundKey.getClass().getName() + "' instead)");
990                 }
991                 if (((Number) foundKey).longValue() == longValue) {
992                     return row;
993                 }
994             }
995         }
996         return null;
997     }
998 
999     
1000     /** Searches a structured list for a particular row. The list is presumed to be a List of Maps,
1001      *  each of which contains a key field. Lists are searched sequentially until the desired row is
1002      *  found. It is permitted to search on null key values. If the row cannot be found, null is
1003      *  returned.
1004      *
1005      *  <p>For example, in the list:
1006      *  <pre>
1007      *  list = [
1008      *    0: { systemId = "abc", systemTimezone = "dalby" }
1009      *    1: { systemId = "def", systemTimezone = "london" }
1010      *    2: { systemId = "ghi", systemTimezone = "new york" }
1011      *  ]
1012      *  </pre>
1013      *
1014      *  <p><code>getStructuredListItem(list, "systemId", "def")</code> would return a reference to the
1015      *  second element in the list, i.e. the Map:
1016      *
1017      *  <pre>
1018      *  { systemId = "def", systemTimezone = "london" }
1019      *  </pre>
1020      *
1021      * @param list The list to search.
1022      * @param keyField The name of the key field.
1023      * @param key The key value to search for
1024      *
1025      * @return The requested element in the list, or null if the element cannot be found.
1026      *
1027      * @throws NullPointerException if list or keyField is set to null.
1028      * @throws IllegalArgumentException if the list is not composed of Maps
1029      */
1030     static public Map getStructuredListItem(List list, String keyField, Object key) {
1031         if (list == null) { throw new NullPointerException("Cannot search null list"); }
1032         if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
1033         Map row;
1034         Object foundKey;
1035         for (Iterator i = list.iterator(); i.hasNext();) {
1036             // could handle generic objects via Codec.getValue(row, keyField); instead
1037             try {
1038                 row = (Map) i.next();
1039             } catch (ClassCastException cce) {
1040                 throw (IllegalArgumentException) new IllegalArgumentException("List must be composed of Maps").initCause(cce);
1041             }
1042             foundKey = row.get(keyField);  
1043             if (foundKey == null && key==null) {
1044                 return row;
1045             } else {
1046                 if (foundKey!=null && foundKey.equals(key)) {
1047                     return row;
1048                 }
1049             }
1050         }
1051         return null;
1052     }
1053     
1054 	// fun fun fun
1055 	 /** As per {@link Struct#getStructuredListItem(List, String, long)}, with a 
1056     * compound key.
1057     * 
1058     * @param list The list to search.
1059     * @param keyField The name of the key field.
1060     * @param longValue The key value to search for
1061     * @param keyField2 The name of the second key field.
1062     * @param longValue2 The second key value to search for
1063     *
1064     * @return The requested element in the list, or null if the element cannot be found.
1065     *
1066     * @throws NullPointerException if list or keyField is set to null.
1067     * @throws IllegalStateException if the list is not composed of Maps
1068     */
1069    static public Map getStructuredListItem2(List list, String keyField, long longValue, String keyField2, long longValue2) {
1070        if (list == null) { throw new NullPointerException("Cannot search null list"); }
1071        if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
1072        if (keyField2 == null) { throw new NullPointerException("Cannot search for null keyField2"); }
1073 
1074        Map row;
1075        Object foundKey, foundKey2;
1076        for (Iterator i = list.iterator(); i.hasNext();) {
1077            try {
1078                row = (Map) i.next();
1079            } catch (ClassCastException cce) {
1080                throw (IllegalStateException) new IllegalStateException("List must be composed of Maps").initCause(cce);
1081            }
1082            foundKey = row.get(keyField);
1083            if (foundKey != null) {
1084                if (!(foundKey instanceof Number)) {
1085                    throw (IllegalStateException) new IllegalStateException("Key is not numeric (found '" + foundKey.getClass().getName() + "' instead)");
1086                }
1087                if (((Number) foundKey).longValue() == longValue) {
1088                	
1089                	foundKey2 = row.get(keyField2);
1090                	if (foundKey2 != null) {
1091                        if (!(foundKey2 instanceof Number)) {
1092                            throw (IllegalStateException) new IllegalStateException("Key2 is not numeric (found '" + foundKey2.getClass().getName() + "' instead)");
1093                        }
1094                	}
1095                    if (((Number) foundKey2).longValue() == longValue2) {
1096                    	return row;
1097                    }
1098                }
1099            }
1100        }
1101        return null;
1102    }
1103    
1104    
1105 
1106 
1107     /** Searches a structured list for a particular row. The list may
1108      * be composed of arbitrary objects.
1109      *
1110      * @param list The list to search.
1111      * @param keyField The name of the key field.
1112      * @param key The key value to search for
1113      *
1114      * @return The requested element in the list, or null if the element cannot be found.
1115      *
1116      * @throws NullPointerException if list or keyField is set to null.
1117      * @throws IllegalArgumentException if the list is not composed of Maps
1118      */
1119 
1120     static public Object getStructuredListObject(List list, String keyField, Object key) {
1121         if (list == null) { throw new NullPointerException("Cannot search null list"); }
1122         if (keyField == null) { throw new NullPointerException("Cannot search for null keyField"); }
1123         Object row;
1124         Object foundKey;
1125         for (Iterator i = list.iterator(); i.hasNext();) {
1126             // could handle generic objects via Codec.getValue(row, keyField); instead
1127             row = i.next();
1128             foundKey = Struct.getValue(row, keyField);  
1129             if (foundKey == null && key==null) {
1130                 return row;
1131             } else {
1132                 if (foundKey!=null && foundKey.equals(key)) {
1133                     return row;
1134                 }
1135             }
1136         }
1137         return null;
1138     }
1139 
1140     
1141     /* It would be nice if this was implemented :) 
1142     public List filterStructuredList(List list, TopLevelExpression expression) {
1143         return null;
1144     }
1145     */
1146 
1147     /** Searches a structured list for a particular column. The list is presumed to be a List of Maps,
1148      *  each of which contains a particular key. The value of this key is retrieved from each
1149      *  Map, and added to a new List, which is then returned to the user. If the
1150      *  value of that key is null for a particular Map, then the null value is added to the
1151      *  returned list.
1152      *
1153      *  <p>For example, in the list:
1154      *  <pre>
1155      *  list = [
1156      *    0: { systemId = "abc", systemTimezone = "dalby" }
1157      *    1: { systemId = "def", systemTimezone = "london" }
1158      *    2: { systemId = "ghi", systemTimezone = "new york" }
1159      *    3: null
1160      *    4: { systemId = "jkl", systemTimezone = "melbourne" }
1161      *  ]
1162      *  </pre>
1163      *
1164      *  <p><code>getStructuredListColumn(list, "systemId")</code> would return a new List with
1165      *  three elements, i.e.
1166      *
1167      *  <pre>
1168      *  list = [
1169      *    0: "abc"
1170      *    1: "def"
1171      *    2: "ghi"
1172      *    3: null
1173      *    4: "jkl"
1174      *  ]
1175      *  </pre>
1176      *
1177      * @param list The list to search.
1178      * @param columnName The key of the entry in each map to retrieve
1179      *
1180      * @return a List of Objects
1181      *
1182      * @throws NullPointerException if list or columnName is set to null.
1183      */
1184     public static List getStructuredListColumn(List list, String columnName) {
1185         if (list == null) {
1186             throw new NullPointerException("list must not be empty");
1187         }
1188 
1189         if (columnName == null) {
1190             throw new NullPointerException("columnName must not be empty");
1191         }
1192 
1193         if (list.size() == 0) {
1194             return Collections.EMPTY_LIST;
1195         }
1196 
1197         ArrayList result = new ArrayList(list.size());
1198         Iterator it = list.iterator();
1199 
1200         while (it.hasNext()) {
1201         	Object obj = it.next();
1202         	if (obj instanceof Map) {
1203                 Map map = (Map) obj;
1204 	            if (map==null) {
1205 	            	result.add(null);
1206 	            } else {
1207 		            Object o = map.get(columnName);
1208 		            result.add(o);
1209 	            }
1210         	} else {
1211         		Object o = getValue(obj, columnName);
1212         		result.add(o);
1213         	}
1214         }
1215 
1216         return result;
1217     }
1218 
1219 
1220     /** This method returns a human-readable version of a structured list.
1221      * The output of this list looks similar to the following:
1222      *
1223      * <pre style="code">
1224      *   topLevelName = [
1225      *     0: 'stringValue'
1226      *     1: null
1227      *     2: (WeirdObjectClass) "toString() output of weirdObject"
1228      *     3 = {
1229      *         mapElement =&gt; ...,
1230      *         ...
1231      *       }
1232      *     4 = [
1233      *         0: listElement
1234      *         ...
1235      *       ]
1236      *     5 = (
1237      *         setElement
1238      *     )
1239      *     ...
1240      *   ]
1241      * </pre>
1242      *
1243      * Strings are represented as their own values in quotes; null values
1244      * are represented with the text <code>null</code>, and structured maps
1245      * and structured lists contained within this list are recursed into.
1246      *
1247      * @param topLevelName The name to assign to the list in the first row of output
1248      * @param list         The list we wish to represent as a string
1249      * @return             A human-readable version of a structured list
1250      */
1251     static public String structuredListToString(String topLevelName, List list) {
1252         String s;
1253         Object value;
1254         int index = 0;
1255 
1256 		if (list==null) {
1257 			return topLevelName + " = (List) null\n";
1258 		}
1259 
1260         s = topLevelName + " = [\n";
1261 
1262         for (Iterator i = list.iterator(); i.hasNext();) {
1263             value = i.next();
1264 
1265             if (value == null) {
1266                 s = s + "  " + index + ": null\n";
1267             } else if (value instanceof String) {
1268                 s = s + "  " + index + ": '" + Text.getDisplayString("", (String) value) + "'\n";
1269 			} else if (value.getClass().isArray()) {
1270 				List wrapper = Arrays.asList((Object[]) value);
1271 				s = s + "  " + index + ": (" + value.getClass() + ") " + Text.indent("  ", structuredListToString(String.valueOf(index), wrapper));
1272             } else if (value instanceof Map) {
1273                 s = s + Text.indent("  ", structuredMapToString(String.valueOf(index), (Map) value));
1274             } else if (value instanceof List) {
1275                 s = s + Text.indent("  ", structuredListToString(String.valueOf(index), (List) value));
1276 			} else if (value instanceof Set) {
1277 				s = s + Text.indent("  ", structuredSetToString(String.valueOf(index), (Set) value));
1278             } else {
1279                 s = s + "  " + index + ": (" + Text.getLastComponent(value.getClass().getName()) + ") " + Text.getDisplayString("", value.toString()) + "\n";
1280             }
1281 
1282             index = index + 1;
1283         }
1284 
1285         s = s + "]\n";
1286 
1287         return s;
1288     }
1289 
1290 	/** This method returns a human-readable version of a structured set.
1291 	 * The output of this set looks similar to the following:
1292 	 *
1293 	 * <pre style="code">
1294 	 *   topLevelName = (
1295 	 *     0: 'stringValue'
1296 	 *     1: null
1297 	 *     2: (WeirdObjectClass) "toString() output of weirdObject"
1298 	 *     3 = {
1299 	 *         mapElement =&gt; ...,
1300 	 *         ...
1301 	 *       }
1302 	 *     4 = [
1303 	 *         0: listElement
1304 	 *         ...
1305 	 *       ]
1306 	 *     5 = (
1307 	 *         setElement
1308 	 *       )
1309 	 *     ...
1310 	 *   )
1311 	 * </pre>
1312 	 *
1313 	 * Strings are represented as their own values in quotes; null values
1314 	 * are represented with the text <code>null</code>, and structured maps
1315 	 * and structured lists contained within this list are recursed into.
1316 	 *
1317 	 * @param topLevelName The name to assign to the list in the first row of output
1318 	 * @param set          The set we wish to represent as a string
1319 	 * @return             A human-readable version of a structured list
1320 	 */
1321 	static public String structuredSetToString(String topLevelName, Set set) {
1322 		String s;
1323 		Object value;
1324 		int index = 0;
1325 
1326 		if (set==null) {
1327 			return topLevelName + " = (Set) null\n";
1328 		}
1329 
1330 		s = topLevelName + " = (\n";
1331 
1332 		for (Iterator i = set.iterator(); i.hasNext();) {
1333 			value = i.next();
1334 
1335 			if (value == null) {
1336 				s = s + "  " + index + ": null\n";
1337 			} else if (value instanceof String) {
1338 				s = s + Text.getDisplayString("", (String) value) + "'\n";
1339 			} else if (value.getClass().isArray()) {
1340 				List wrapper = Arrays.asList((Object[]) value);
1341 				s = s + Text.indent("  ", structuredListToString(String.valueOf(index), wrapper));
1342 			} else if (value instanceof Map) {
1343 				s = s + Text.indent("  ", structuredMapToString(String.valueOf(index), (Map) value));
1344 			} else if (value instanceof List) {
1345 				s = s + Text.indent("  ", structuredListToString(String.valueOf(index), (List) value));
1346 			} else if (value instanceof Set) {
1347 				s = s + Text.indent("  ", structuredSetToString(String.valueOf(index), (Set) value));
1348 			} else {
1349 				s = s + "  (" + Text.getLastComponent(value.getClass().getName()) + ") " + Text.getDisplayString("", value.toString()) + "\n";
1350 			}
1351 
1352 			index = index + 1;
1353 		}
1354 
1355 		s = s + ")\n";
1356 
1357 		return s;
1358 	}
1359 
1360     /** This method returns a human-readable version of a structured map.
1361      * The output of this list looks similar to the following:
1362      *
1363      * <pre style="code">
1364      *   topLevelName = {
1365      *     apples =&gt; 'stringValue'
1366      *     rhinocerouseses =&gt; null
1367      *     weirdObjectValue =&gt; (WeirdObjectClass) "toString() output of weirdObject"
1368      *     mapValue = {
1369      *         mapElement =&gt; ...
1370      *         ...
1371      *       }
1372      *     listValue = [
1373      *         0: listElement
1374      *         ...
1375      *       ]
1376      *     setValue = (
1377      *         setElement
1378      *       )
1379      *     ...
1380      *   ]
1381      * </pre>
1382      *
1383      * Keys within the map are sorted alphabetically before being enumerated.
1384      *
1385      * Strings are represented as their own values in quotes; null values
1386      * are represented with the text <code>null</code>, and structured maps
1387      * and structured lists contained within this list are recursed into.
1388      *
1389      * @param topLevelName The name to assign to the list in the first row of output
1390      * @param map          The map we wish to represent as a string
1391      * @return             A human-readable version of a structured map
1392      */
1393     static public String structuredMapToString(String topLevelName, Map map) {
1394         String s;
1395         String key;
1396         Object value;
1397 
1398 		if (map==null) {
1399 			return topLevelName + " = (Map) null\n";
1400 		}
1401 
1402 		// sort map output by key name
1403         List list = new ArrayList(map.keySet());
1404         Collections.sort(list, new ListContainingNullComparator());
1405 
1406         s = topLevelName + " = {\n";
1407 
1408         for (Iterator i = list.iterator(); i.hasNext();) {
1409             key = (String) i.next();
1410             value = map.get(key);
1411 
1412             if (value == null) {
1413                 s = s + "  " + ((key == null) ? "null" : key) + " => null\n";
1414             } else if (value instanceof String) {
1415                 s = s + "  " + ((key == null) ? "null" : key) + " => '" + Text.getDisplayString(key, (String) value) + "'\n";
1416 			} else if (value.getClass().isArray()) {
1417 				//List wrapper = Arrays.asList((Object[]) value);
1418 				//s = s + Text.indent("  ", structuredListToString(key, wrapper));
1419 				if (value instanceof Object[]) {
1420 	            	List wrapper = Arrays.asList((Object[])value);
1421 	            	s = s + Text.indent("  ", structuredListToString(key, wrapper));
1422             	} else if (value instanceof double[]) {
1423             		// @TODO other primitive array types
1424             		// @TODO convert directly probably
1425             		// @XXX this displays primitive double types as (Double), not (double)
1426             		double[] daSrc = (double[]) value;
1427             		Double[] daTgt = new Double[daSrc.length];
1428             		for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1429             		List arrayList = Arrays.asList((Object[])daTgt);
1430             		s = s + Text.indent("  ", structuredListToString(key, arrayList));
1431             	} else if (value instanceof int[]) {
1432             		int[] daSrc = (int[]) value;
1433             		Integer[] daTgt = new Integer[daSrc.length];
1434             		for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1435             		List arrayList = Arrays.asList((Object[])daTgt);
1436             		s = s + Text.indent("  ", structuredListToString(key, arrayList));
1437 
1438             	} else {
1439             		throw new UnsupportedOperationException("Cannot convert primitive array to String");
1440             	}
1441 				
1442             } else if (value instanceof Map) {
1443                 s = s + Text.indent("  ", structuredMapToString(key, (Map) value));
1444             } else if (value instanceof List) {
1445                 s = s + Text.indent("  ", structuredListToString(key, (List) value));
1446 			} else if (value instanceof Set) {
1447 				s = s + Text.indent("  ", structuredSetToString(key, (Set) value));
1448             } else {
1449                 s = s + "  " + ((key == null) ? "null" : key) + " => (" + Text.getLastComponent(value.getClass().getName()) + ") " + Text.getDisplayString(key, value.toString()) + "\n";
1450             }
1451         }
1452 
1453         s = s + "}\n";
1454 
1455         return s;
1456     }
1457 
1458 
1459 	/** This method returns a human-readable version of an array object that contains
1460 	 * structured lists/maps/sets. Bear in mind that the array object itself isn't 
1461 	 * considered 'structured' by the definitions at the top of this class, or
1462 	 * at least, it won't until this is added to the other structured parsers/readers.
1463 	 *
1464 	 * <pre style="code">
1465 	 *   topLevelName[] = [
1466 	 *     0: 'stringValue'
1467 	 *     1: null
1468 	 *     2: (WeirdObjectClass) "toString() output of weirdObject"
1469 	 *     3 = {
1470 	 *         mapElement =&gt; ...,
1471 	 *         ...
1472 	 *       }
1473 	 *     4 = [
1474 	 *         0: listElement
1475 	 *         ...
1476 	 *       ]
1477 	 *     5 = (
1478 	 *         setElement
1479 	 *     )
1480 	 *     ...
1481 	 *   ]
1482 	 * </pre>
1483 	 *
1484 	 * Strings are represented as their own values in quotes; null values
1485 	 * are represented with the text <code>null</code>, and structured maps
1486 	 * and structured lists contained within this list are recursed into.
1487 	 *
1488 	 * @param topLevelName The name to assign to the list in the first row of output
1489 	 * @param list         The list we wish to represent as a string
1490 	 * @return             A human-readable version of a structured list
1491 	 */
1492 	static public String arrayToString(String topLevelName, Object list) {
1493 		if (list==null) {
1494 			return topLevelName + "[] = (Array) null\n";
1495 		}
1496 		if (!list.getClass().isArray()) {
1497 			throw new IllegalArgumentException("object must be array");
1498 		}
1499 		List wrapper = Arrays.asList((Object[]) list);
1500 		return structuredListToString(topLevelName + "[]", wrapper); 
1501 	}
1502 
1503 	
1504     /**
1505      * Converts a java List into javascript
1506      *
1507      * @param list the list to convert into javascript
1508      *
1509      * @return the javascript version of this list.
1510      */
1511     static public String structuredListToJson(List list)
1512     {
1513     	return structuredListToJson(list, "microsoft");
1514     }
1515     
1516 	/**
1517      * Converts a java List into javascript
1518      *
1519      * @param list the list to convert into javascript
1520      *
1521      * @return the javascript version of this list.
1522      */
1523     public static String structuredListToJson(List list, String jsonFormat)
1524     {
1525         StringBuilderWriter w = new StringBuilderWriter(list.size() * 2);
1526         try {
1527         	structuredListToJson(w, list, jsonFormat);
1528 		} catch (IOException e) {
1529 			throw new IllegalStateException("IOException in StringBuilderWriter", e);
1530 		}
1531         return w.toString();
1532     }
1533     
1534     public static void structuredListToJson(Writer w, List list, String jsonFormat) throws IOException {
1535     	if (list==null) {
1536     		w.append("null");
1537     		return;
1538     	}
1539     	
1540         Object value;
1541         int index = 0;
1542         w.append('[');
1543         for (Iterator i = list.iterator(); i.hasNext(); ) {
1544             value = i.next();
1545             if (value == null) {
1546                 w.append("null");
1547             } else if (value instanceof String) {
1548                 w.append('\"').append(Text.escapeJavascript((String) value)).append('\"'); 
1549             } else if (value instanceof WriteJsonFormat) {
1550             	((WriteJsonFormat) value).writeJsonFormat(w,  jsonFormat);
1551             } else if (value instanceof ToJsonFormat) {
1552             	w.append(((ToJsonFormat) value).toJson(jsonFormat));
1553             } else if (value instanceof ToJson) {
1554             	w.append(((ToJson) value).toJson());
1555             } else if (value instanceof ToStringReturnsJson) {
1556             	w.append(value.toString());
1557             } else if (value instanceof Map) {
1558                 structuredMapToJson(w, (Map) value, jsonFormat); // @TODO pass in stringbuffer to pvt method
1559             } else if (value instanceof List) {
1560                 structuredListToJson(w, (List) value, jsonFormat);
1561             } else if (value instanceof Number) {
1562                 w.append(value.toString());
1563             } else if (value instanceof Boolean) {
1564                 w.append(value.toString());
1565             } else if (value instanceof java.util.Date) {
1566             	// MS-compatible JSON encoding of Dates:
1567             	// see http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx
1568                 w.append(toDate((java.util.Date)value, jsonFormat));
1569             } else if (value.getClass().isArray()) {
1570            	  	if (value instanceof Object[]) {
1571            	  		List arrayList = Arrays.asList((Object[])value);
1572            	  		structuredListToJson(w, (List)arrayList, jsonFormat);
1573            	  	} else if (value instanceof double[]) {
1574 	              	// @TODO other primitive array types
1575 	           		// @TODO convert directly probably
1576 	           		double[] daSrc = (double[]) value;
1577 	           		Double[] daTgt = new Double[daSrc.length];
1578 	           		for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1579 	           		List arrayList = Arrays.asList((Object[])daTgt);
1580 	           		structuredListToJson(w, (List) arrayList, jsonFormat);
1581            	  	} else if (value instanceof int[]) {
1582 	           		int[] daSrc = (int[]) value;
1583 	           		Integer[] daTgt = new Integer[daSrc.length];
1584 	           		for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1585 	           		List arrayList = Arrays.asList((Object[])daTgt);
1586 	           		structuredListToJson(w, (List) arrayList, jsonFormat);
1587            	  	} else {
1588            	  		throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1589            	  	}
1590             } else {
1591                 throw new RuntimeException("Cannot translate Java object " +
1592                     value.getClass().getName() + " to javascript value");
1593             }
1594             index = index + 1;
1595             if (i.hasNext()) {
1596                 w.append(',');
1597             }
1598         }
1599         w.append("]\n");
1600         // return s.toString();
1601     }
1602 
1603      /** Converts a java map into javascript  
1604      *
1605      * @param map the Map to convert into javascript
1606      *
1607      * @return a javascript version of this Map
1608      */
1609     static public String structuredMapToJson(Map map)
1610     {
1611    		return structuredMapToJson(map, DATE_FORMAT_MICROSOFT);
1612     }
1613 
1614     
1615     /** Converts a java map into javascript  
1616      *
1617      * @param map the Map to convert into javascript
1618      *
1619      * @return a javascript version of this Map
1620      */
1621     public static String structuredMapToJson(Map map, String jsonFormat) {
1622 	    StringBuilderWriter w = new StringBuilderWriter();
1623 	    try {
1624 			structuredMapToJson(w, map, jsonFormat);
1625 		} catch (IOException e) {
1626 			throw new IllegalStateException("IOException in StringBuilderWriter", e);
1627 		}
1628 	    return w.toString();
1629     }
1630    
1631     public static void structuredMapToJson(Writer w, Map map, String jsonFormat) throws IOException {
1632     	if (map==null) {
1633     		w.append("null");
1634     		return;
1635     	}
1636     	
1637        Map.Entry entry;
1638        // String s;
1639        Object key;
1640        String keyJson;
1641        Object value;
1642        List list = new ArrayList(map.keySet());
1643 
1644        Collections.sort(list, new ListComparator());
1645        boolean isFirst = true;
1646 
1647        w.append("{");
1648 
1649        for (Iterator i = list.iterator(); i.hasNext();) {
1650            key = (Object) i.next();
1651            if (key instanceof String) {
1652         	   keyJson = "\"" + Text.escapeJavascript((String) key) + "\"";
1653            } else if (key instanceof Number) {
1654         	   keyJson = "\"" + String.valueOf(key) + "\""; // coerce numeric keys to strings
1655            } else {
1656         	   throw new IllegalArgumentException("Cannot convert key type " + key.getClass().getName() + " to javascript value");
1657            }
1658            value = map.get(key);
1659            if (key == null || key.equals("")) {
1660                continue; // don't allow null or empty keys, 
1661            } else if (value == null) {
1662                continue; // don't bother transferring null values to javascript
1663            } else if (value instanceof String) {
1664         	   if (!isFirst) { w.append(","); }
1665         	   w.append(keyJson);
1666         	   w.append( ": \"");
1667         	   w.append(Text.escapeJavascript((String) value));
1668         	   w.append("\"");
1669            } else if (value instanceof WriteJsonFormat) {
1670         	   if (!isFirst) { w.append(","); }
1671         	   w.append(keyJson);
1672         	   w.append(": ");
1673         	   ((WriteJsonFormat) value).writeJsonFormat(w,  jsonFormat);
1674            } else if (value instanceof ToJsonFormat) {
1675         	   if (!isFirst) { w.append(","); }
1676         	   w.append(keyJson);
1677         	   w.append(": ");
1678         	   w.append(((ToJsonFormat) value).toJson(jsonFormat));
1679            } else if (value instanceof ToJson) {
1680         	   if (!isFirst) { w.append(","); }
1681         	   w.append(keyJson);
1682         	   w.append(": ");
1683         	   w.append(((ToJson) value).toJson());
1684            } else if (value instanceof ToStringReturnsJson) {
1685         	   if (!isFirst) { w.append(","); }
1686         	   w.append(keyJson);
1687         	   w.append(": ");
1688         	   w.append(value.toString());
1689            } else if (value instanceof Map) {
1690         	   if (!isFirst) { w.append(","); }
1691                w.append(keyJson);
1692                w.append(": ");
1693                structuredMapToJson(w, (Map) value, jsonFormat);
1694            } else if (value instanceof List) {
1695         	   if (!isFirst) { w.append(","); }
1696         	   w.append(keyJson);
1697         	   w.append(": ");
1698         	   structuredListToJson(w, (List) value, jsonFormat);
1699            } else if (value instanceof Number) {
1700         	   if (!isFirst) { w.append(","); }
1701                w.append(keyJson);
1702                w.append(": ");
1703                w.append(value.toString());
1704            } else if (value instanceof Boolean) {
1705         	   if (!isFirst) { w.append(","); }
1706         	   w.append(keyJson);
1707         	   w.append(": ");
1708         	   w.append(value.toString());
1709            } else if (value instanceof java.util.Date) {
1710            	// MS-compatible JSON encoding of Dates:
1711            	// see http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx
1712         	   if (!isFirst) { w.append(","); }
1713                w.append(keyJson);
1714                w.append(": ");
1715                w.append(toDate((java.util.Date)value, jsonFormat));
1716            } else if (value.getClass().isArray()) {
1717         	   
1718            	  if (value instanceof Object[]) {
1719 	              List arrayList = Arrays.asList((Object[])value);
1720 	              if (!isFirst) { w.append(","); }
1721 	              w.append(keyJson);
1722 	              w.append(": ");
1723 	              structuredListToJson(w, (List)arrayList, jsonFormat);
1724            	  } else if (value instanceof double[]) {
1725               	  // @TODO other primitive array types
1726             	  // @TODO convert directly probably
1727            		  double[] daSrc = (double[]) value;
1728            		  Double[] daTgt = new Double[daSrc.length];
1729            		  for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1730            		  List arrayList = Arrays.asList((Object[])daTgt);
1731            		  if (!isFirst) { w.append(","); }
1732            		  w.append(keyJson);
1733            		  w.append(": ");
1734            		  structuredListToJson(w, (List)arrayList, jsonFormat);
1735            	  } else if (value instanceof int[]) {
1736               	  // @TODO other primitive array types
1737             	  // @TODO convert directly probably
1738            		  int[] daSrc = (int[]) value;
1739            		  Integer[] daTgt = new Integer[daSrc.length];
1740            		  for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1741            		  List arrayList = Arrays.asList((Object[])daTgt);
1742            		  if (!isFirst) { w.append(","); }
1743            		  w.append(keyJson);
1744            		  w.append(": ");
1745            		  structuredListToJson(w, (List)arrayList, jsonFormat);
1746            	  } else if (value instanceof long[]) {
1747            		  long[] daSrc = (long[]) value;
1748            		  Long[] daTgt = new Long[daSrc.length];
1749            		  for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1750            		  List arrayList = Arrays.asList((Object[])daTgt);
1751            		  if (!isFirst) { w.append(","); }
1752            		  w.append(keyJson);
1753            		  w.append(": ");
1754            		  structuredListToJson(w, (List)arrayList, jsonFormat);
1755            	  } else if (value instanceof byte[]) {
1756            		  byte[] daSrc = (byte[]) value;
1757            		  Byte[] daTgt = new Byte[daSrc.length];
1758            		  for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1759            		  List arrayList = Arrays.asList((Object[])daTgt);
1760            		  if (!isFirst) { w.append(","); }
1761            		  w.append(keyJson);
1762            		  w.append(": ");
1763            		  structuredListToJson(w, (List)arrayList, jsonFormat);
1764            		  
1765            	  } else {
1766            		  throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1767            	  }
1768            } else {
1769                throw new RuntimeException("Cannot translate Java object " + value.getClass().getName() + " to javascript value");
1770            }
1771            isFirst = false;
1772        }
1773 
1774        w.append("}\n");
1775        // return s;
1776    }
1777 	
1778 	
1779 	/**
1780      * Converts a java List into javascript, whilst filtering the keys of any Maps to only those in validKeys
1781      *
1782      * @param list the list to convert into javascript
1783      * @param jsonFormat the jsonFormat
1784      * @param validKeys 
1785      *
1786      * @return the javascript version of this list.
1787      */
1788     public static String structuredListToFilteredJson(List list, String jsonFormat, String...validKeys) {
1789         StringBuilderWriter w = new StringBuilderWriter(list.size() * 2);
1790         try {
1791         	structuredListToFilteredJson(w, list, jsonFormat, validKeys);
1792 		} catch (IOException e) {
1793 			throw new IllegalStateException("IOException in StringBuilderWriter", e);
1794 		}
1795         return w.toString();
1796     }
1797 
1798 	/**
1799      * Converts a java Map into javascript, whilst filtering the keys of any Maps to only those in validKeys
1800      *
1801      * @param map the map to convert into javascript
1802      * @param jsonFormat the jsonFormat
1803      * @param validKeys 
1804      *
1805      * @return the javascript version of this list.
1806      */
1807     public static String structuredMapToFilteredJson(Map map, String jsonFormat, String...validKeys) {
1808         StringBuilderWriter w = new StringBuilderWriter();
1809         try {
1810         	structuredMapToFilteredJson(w, map, jsonFormat, validKeys);
1811 		} catch (IOException e) {
1812 			throw new IllegalStateException("IOException in StringBuilderWriter", e);
1813 		}
1814         return w.toString();
1815     }
1816 
1817     
1818     public static void structuredListToFilteredJson(Writer w, List list, String jsonFormat, String... validKeys) throws IOException {
1819         Object value;
1820         int index = 0;
1821         w.append('[');
1822         for (Iterator i = list.iterator(); i.hasNext(); ) {
1823             value = i.next();
1824             if (value == null) {
1825                 w.append("null");
1826             } else if (value instanceof String) {
1827                 w.append('\"').append(Text.escapeJavascript((String) value)).append('\"'); 
1828             } else if (value instanceof ToJsonFormat) {
1829             	w.append(((ToJsonFormat) value).toJson(jsonFormat));
1830             } else if (value instanceof ToJson) {
1831             	w.append(((ToJson) value).toJson());
1832             } else if (value instanceof ToStringReturnsJson) {
1833             	w.append(value.toString());
1834             } else if (value instanceof Map) {
1835                 structuredMapToFilteredJson(w, (Map) value, jsonFormat, validKeys); // @TODO pass in stringbuffer to pvt method
1836             } else if (value instanceof List) {
1837                 structuredListToFilteredJson(w, (List) value, jsonFormat, validKeys);
1838             } else if (value instanceof Number) {
1839                 w.append(value.toString());
1840             } else if (value instanceof Boolean) {
1841                 w.append(value.toString());
1842             } else if (value instanceof java.util.Date) {
1843             	// MS-compatible JSON encoding of Dates:
1844             	// see http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx
1845                 w.append(Struct.toDate((java.util.Date)value, jsonFormat));
1846             } else if (value.getClass().isArray()) {
1847            	  	if (value instanceof Object[]) {
1848            	  		List arrayList = Arrays.asList((Object[])value);
1849            	  		structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1850            	  	} else if (value instanceof double[]) {
1851 	              	// @TODO other primitive array types
1852 	           		// @TODO convert directly probably
1853 	           		double[] daSrc = (double[]) value;
1854 	           		Double[] daTgt = new Double[daSrc.length];
1855 	           		for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1856 	           		List arrayList = Arrays.asList((Object[])daTgt);
1857 	           		structuredListToFilteredJson(w, (List) arrayList, jsonFormat, validKeys);
1858            	  	} else if (value instanceof int[]) {
1859 	           		int[] daSrc = (int[]) value;
1860 	           		Integer[] daTgt = new Integer[daSrc.length];
1861 	           		for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1862 	           		List arrayList = Arrays.asList((Object[])daTgt);
1863 	           		structuredListToFilteredJson(w, (List) arrayList, jsonFormat, validKeys);
1864            	  	} else {
1865            	  		throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1866            	  	}
1867             } else {
1868                 throw new RuntimeException("Cannot translate Java object " +
1869                     value.getClass().getName() + " to javascript value");
1870             }
1871             index = index + 1;
1872             if (i.hasNext()) {
1873                 w.append(',');
1874             }
1875         }
1876         w.append("]");  // removed \n
1877     }
1878 
1879    public static void structuredMapToFilteredJson(Writer w, Map map, String jsonFormat, String... validKeys) throws IOException {
1880        // String s;
1881        String keyJson = null;
1882        Object value;
1883        
1884        /* List list = new ArrayList(map.keySet());
1885        Collections.sort(list, new ListComparator());
1886        */
1887        
1888        boolean isFirst = true;
1889 
1890        w.append("{");
1891 
1892        for (String key : validKeys) {
1893            // key = (Object) i.next();
1894     	   value = map.get(key);
1895     	   if (value != null) {
1896 	           if (key instanceof String) {
1897 	        	   keyJson = "\"" + key + "\"";
1898 	           } else {
1899 	        	   throw new IllegalArgumentException("Cannot convert key type " + key.getClass().getName() + " to javascript value");
1900 	           }
1901     	   }
1902            if (key == null || key.equals("")) {
1903                continue; // don't allow null or empty keys, 
1904            } else if (value == null) {
1905                continue; // don't bother transferring null values to javascript
1906            } else if (value instanceof String) {
1907         	   if (!isFirst) { w.append(","); }
1908         	   w.append(keyJson);
1909         	   w.append( ": \"");
1910         	   w.append(Text.escapeJavascript((String) value));
1911         	   w.append("\"");
1912            } else if (value instanceof ToJsonFormat) {
1913         	   if (!isFirst) { w.append(","); }
1914         	   w.append(keyJson);
1915         	   w.append(": ");
1916         	   w.append(((ToJsonFormat) value).toJson(jsonFormat));
1917            } else if (value instanceof ToJson) {
1918         	   if (!isFirst) { w.append(","); }
1919         	   w.append(keyJson);
1920         	   w.append(": ");
1921         	   w.append(((ToJson) value).toJson());
1922            } else if (value instanceof ToStringReturnsJson) {
1923         	   if (!isFirst) { w.append(","); }
1924         	   w.append(keyJson);
1925         	   w.append(": ");
1926         	   w.append(value.toString());
1927            } else if (value instanceof Map) {
1928         	   if (!isFirst) { w.append(","); }
1929                w.append(keyJson);
1930                w.append(": ");
1931                structuredMapToFilteredJson(w, (Map) value, jsonFormat, validKeys);
1932            } else if (value instanceof List) {
1933         	   if (!isFirst) { w.append(","); }
1934         	   w.append(keyJson);
1935         	   w.append(": ");
1936         	   structuredListToFilteredJson(w, (List) value, jsonFormat, validKeys);
1937            } else if (value instanceof Number) {
1938         	   if (!isFirst) { w.append(","); }
1939                w.append(keyJson);
1940                w.append(": ");
1941                w.append(value.toString());
1942            } else if (value instanceof Boolean) {
1943         	   if (!isFirst) { w.append(","); }
1944         	   w.append(keyJson);
1945         	   w.append(": ");
1946         	   w.append(value.toString());
1947            } else if (value instanceof java.util.Date) {
1948            	// MS-compatible JSON encoding of Dates:
1949            	// see http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx
1950         	   if (!isFirst) { w.append(","); }
1951                w.append(keyJson);
1952                w.append(": ");
1953                w.append(Struct.toDate((java.util.Date)value, jsonFormat));
1954            } else if (value.getClass().isArray()) {
1955         	   
1956            	  if (value instanceof Object[]) {
1957 	              List arrayList = Arrays.asList((Object[])value);
1958 	              if (!isFirst) { w.append(","); }
1959 	              w.append(keyJson);
1960 	              w.append(": ");
1961 	              structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1962            	  } else if (value instanceof double[]) {
1963               	  // @TODO other primitive array types
1964             	  // @TODO convert directly probably
1965            		  double[] daSrc = (double[]) value;
1966            		  Double[] daTgt = new Double[daSrc.length];
1967            		  for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1968            		  List arrayList = Arrays.asList((Object[])daTgt);
1969            		  if (!isFirst) { w.append(","); }
1970            		  w.append(keyJson);
1971            		  w.append(": ");
1972            		  structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1973            	  } else if (value instanceof int[]) {
1974               	  // @TODO other primitive array types
1975             	  // @TODO convert directly probably
1976            		  int[] daSrc = (int[]) value;
1977            		  Integer[] daTgt = new Integer[daSrc.length];
1978            		  for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1979            		  List arrayList = Arrays.asList((Object[])daTgt);
1980            		  if (!isFirst) { w.append(","); }
1981            		  w.append(keyJson);
1982            		  w.append(": ");
1983            		  structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1984            	  } else {
1985            		  throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1986            	  }
1987            } else {
1988                throw new RuntimeException("Cannot translate Java object " + value.getClass().getName() + " to javascript value");
1989            }
1990            isFirst = false;
1991        }
1992 
1993        w.append("}");  // removed \n
1994    }
1995 
1996    
1997     
1998     /** Convert a date object to it's JSON representation.
1999      * 
2000      * <p>As there's no real standard for this, a type format is used to define what kind of Dates your 
2001      * going to get on the Json side.
2002      * 
2003      * @param d a Date object
2004      * @param jsonFormat either "microsoft" or "numeric"
2005      * 
2006      * @return A date in json representation.
2007      * 
2008      * @see <a href="http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx">http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx</a>
2009      */
2010     static public String toDate(Date d, String jsonFormat) {
2011 	   	if (jsonFormat==null || jsonFormat.equals("microsoft")) {
2012 	   		return "\"\\/Date(" + d.getTime() +  ")\\/\"";	
2013 	   	} else {
2014 	   		return String.valueOf(d.getTime());
2015 	   	}
2016     }
2017 
2018 
2019 
2020 
2021     
2022     /** Create a structured Map out of key / value pairs. Keys must be Strings.
2023      * 
2024      * <p>So the value of <code>Struct.newStructuredMap("thing", 1L, "otherThing", "value");</code> is a map
2025      * with two entries:
2026      * <ul><li>an entry with key "thing" and value new Long(1), and 
2027      * <li>an entry  with key "otherThing" and value "value".
2028      * </ul> 
2029      * 
2030      * @param keyValuePairs a list of key / value pairs 
2031      * 
2032      * @return a Map as described above
2033      * 
2034      * @throws ClassCastException if any of the supplied keys is not a String
2035      */
2036     // looking forward to when statically defined Maps become a Java language construct
2037     static public Map<String, Object> newStructuredMap(Object... keyValuePairs) {
2038     	if (keyValuePairs.length%2==1) { throw new IllegalArgumentException("keyValuePairs must have even number of elements"); }
2039     	Map<String, Object> map = new HashMap();
2040     	for (int i=0; i<keyValuePairs.length; i+=2) {
2041     		String key = (String) keyValuePairs[i];
2042     		map.put(key, keyValuePairs[i + 1]);
2043     	}
2044     	return map;
2045     }
2046 
2047 	/** Rename a structured column, as returned from a Spring JdbcTemplate query. This method will iterate
2048 	 * throw all rows of a table, replacing any srcColumnName keys with destColumnName
2049 	 * 
2050 	 * @param rows the table being modified
2051 	 * @param srcColumnName the name of the row key (column) being replaced
2052 	 * @param destColumnName the new name of the row key (column)
2053 	 */
2054 	public static void renameStructuredListColumn(List<Map<String, Object>> rows, String srcColumnName, String destColumnName) {
2055 		if (srcColumnName == null) { throw new NullPointerException("null srcColumnName"); }
2056 		if (destColumnName == null) { throw new NullPointerException("null destColumnName"); }
2057 		if (srcColumnName.equals(destColumnName)) { return; }
2058 		
2059 		for (int i=0; i<rows.size(); i++) {
2060 			Map<String,Object> row = rows.get(i);
2061 			if (row.containsKey(srcColumnName)) {
2062 				row.put(destColumnName, row.get(srcColumnName));
2063 				row.remove(srcColumnName);
2064 			}
2065 		}
2066 	}
2067 
2068 	/** Add a structured column containing a constant value. This method will iterate through all 
2069 	 * rows of a table, adding a new newColumnName key with the supplied value.
2070 	 * 
2071 	 * @param rows the table being modified
2072 	 * @param newColumnName the name of the row key (column) being added
2073 	 * @param value the value to add to the row
2074 	 */
2075 	public static void addStructuredListColumn(List<Map<String, Object>> rows, String newColumnName, Object value) {
2076 		for (int i=0; i<rows.size(); i++) {
2077 			Map<String,Object> row = rows.get(i);
2078 			row.put(newColumnName, value);
2079 		}
2080 	}
2081 
2082     static {
2083         // NB: these used to be ConcurrentReaderHashMaps, but I didn't want to
2084         // bring concurrent.jar into the classpath.
2085         gettersCache = new ConcurrentHashMap();
2086         settersCache = new ConcurrentHashMap();
2087     }
2088 }