001package com.randomnoun.common;
002
003import java.io.IOException;
004import java.io.Writer;
005
006/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
007 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
008 */
009
010import java.lang.reflect.InvocationTargetException;
011import java.lang.reflect.Method;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.Collections;
015import java.util.Comparator;
016import java.util.Date;
017import java.util.HashMap;
018import java.util.Iterator;
019import java.util.List;
020import java.util.Map;
021import java.util.Set;
022import java.util.concurrent.ConcurrentHashMap;
023
024import jakarta.servlet.http.HttpServletRequest;
025
026import org.apache.commons.beanutils.PropertyUtils;
027
028import com.randomnoun.common.io.StringBuilderWriter;
029
030/**
031 * Encoder/decoder of JavaBeans and 'structured' maps and lists. A structured
032 * map or list is any Map or List that satisfies certain conventions, listed below.
033 *
034 * <p>A structured map is any implementation of the {@link java.util.Map}
035 * interface, in which every key is a {@link String}, and every value is either a
036 * primitive wrapper type ({@link String}, {@link Long},
037 * {@link Integer}, etc...), a structured map or a structured list.
038 * A structured list is any implementation of the {@link java.util.List} interface,
039 * of which every value is a primitive wrapper type, a structured map or a structured list.
040 *
041 * <p>In this way, arbitrarily complex objects can be created and passed between
042 * Struts code and the business layer, or Struts code and the JSP layer,
043 * without resorting to the creation of application-specific
044 * datatypes. These datatypes are also used by the Spring JdbcTemplate framework to
045 * return values from a database, so it is useful to have some generic functions
046 * that operate on them.
047 *
048 * 
049 * @author knoxg
050 */
051public class Struct {
052
053    /** A ConcurrentReaderHashMap that maps Class objects to Maps, each of which
054     *  maps getter method names (e.g. getabcd) to Method objects. Method names should be
055     *  lower-cased when elements are added or looked up in this map, to provide
056     *  case-insensitivity.
057     */
058    private static Map<Class<?>, Map<String,Method>> gettersCache;
059 
060    /** A ConcurrentReaderHashMap that maps Class objects to Maps, each of which
061     *  maps setter method names (e.g. setabcd) to Method objects. Method names should be
062     *  lower-cased when elements are added or looked up in this map, to provide
063     *  case-insensitivity.
064     */
065    private static Map<Class<?>, Map<String,Method>> settersCache;
066
067    /** Serialise Date objects using the Microsoft convention for Dates, 
068     * which is a String in the form <code>"/Date(millisSinceEpoch)/"</code>
069     */
070        public static final String DATE_FORMAT_MICROSOFT = "microsoft";
071        
072        /** Serialise Date objects as milliseconds since the epoch */
073        public static final String DATE_FORMAT_NUMERIC = "numeric";
074
075        /** This class can be serialised as a JSON value by calling it's toString() method */ 
076        public static interface ToStringReturnsJson { }
077        
078        /** This class can be serialised as a JSON value by calling it's toJson() method */
079        public static interface ToJson { 
080                public String toJson(); 
081        }
082        
083        /** This class can be serialised as a JSON value by calling it's toJson(String) method. 
084     * Multiple json formats are supported by supplying a jsonFormat string; e.g. 'simple'. 
085     * Passing null or an empty string should be equivalent to calling toJson() if the class also implements the Struct.ToJson interface.
086     */
087        public static interface ToJsonFormat { 
088                public String toJson(String jsonFormat); 
089        }
090        
091        /** This class can be serialised as a JSON value by calling it's writeJsonFormat() method 
092     * Multiple json formats are supported by supplying a jsonFormat string; e.g. 'simple'. 
093     * Passing null or an empty string should be equivalent to calling toJson() if the class also implements the Struct.ToJson interface.
094         */
095        public static interface WriteJsonFormat { 
096                public void writeJsonFormat(Writer w, String jsonFormat) throws IOException; 
097        }
098        
099        // @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 {
1747                          throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1748                  }
1749           } else {
1750               throw new RuntimeException("Cannot translate Java object " + value.getClass().getName() + " to javascript value");
1751           }
1752           isFirst = false;
1753       }
1754
1755       w.append("}\n");
1756       // return s;
1757   }
1758        
1759        
1760        /**
1761     * Converts a java List into javascript, whilst filtering the keys of any Maps to only those in validKeys
1762     *
1763     * @param list the list to convert into javascript
1764     * @param jsonFormat the jsonFormat
1765     * @param validKeys 
1766     *
1767     * @return the javascript version of this list.
1768     */
1769    public static String structuredListToFilteredJson(List list, String jsonFormat, String...validKeys) {
1770        StringBuilderWriter w = new StringBuilderWriter(list.size() * 2);
1771        try {
1772                structuredListToFilteredJson(w, list, jsonFormat, validKeys);
1773                } catch (IOException e) {
1774                        throw new IllegalStateException("IOException in StringBuilderWriter", e);
1775                }
1776        return w.toString();
1777    }
1778
1779        /**
1780     * Converts a java Map into javascript, whilst filtering the keys of any Maps to only those in validKeys
1781     *
1782     * @param map the map 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 structuredMapToFilteredJson(Map map, String jsonFormat, String...validKeys) {
1789        StringBuilderWriter w = new StringBuilderWriter();
1790        try {
1791                structuredMapToFilteredJson(w, map, jsonFormat, validKeys);
1792                } catch (IOException e) {
1793                        throw new IllegalStateException("IOException in StringBuilderWriter", e);
1794                }
1795        return w.toString();
1796    }
1797
1798    
1799    public static void structuredListToFilteredJson(Writer w, List list, String jsonFormat, String... validKeys) throws IOException {
1800        Object value;
1801        int index = 0;
1802        w.append('[');
1803        for (Iterator i = list.iterator(); i.hasNext(); ) {
1804            value = i.next();
1805            if (value == null) {
1806                w.append("null");
1807            } else if (value instanceof String) {
1808                w.append('\"').append(Text.escapeJavascript((String) value)).append('\"'); 
1809            } else if (value instanceof ToJsonFormat) {
1810                w.append(((ToJsonFormat) value).toJson(jsonFormat));
1811            } else if (value instanceof ToJson) {
1812                w.append(((ToJson) value).toJson());
1813            } else if (value instanceof ToStringReturnsJson) {
1814                w.append(value.toString());
1815            } else if (value instanceof Map) {
1816                structuredMapToFilteredJson(w, (Map) value, jsonFormat, validKeys); // @TODO pass in stringbuffer to pvt method
1817            } else if (value instanceof List) {
1818                structuredListToFilteredJson(w, (List) value, jsonFormat, validKeys);
1819            } else if (value instanceof Number) {
1820                w.append(value.toString());
1821            } else if (value instanceof Boolean) {
1822                w.append(value.toString());
1823            } else if (value instanceof java.util.Date) {
1824                // MS-compatible JSON encoding of Dates:
1825                // see http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx
1826                w.append(Struct.toDate((java.util.Date)value, jsonFormat));
1827            } else if (value.getClass().isArray()) {
1828                        if (value instanceof Object[]) {
1829                                List arrayList = Arrays.asList((Object[])value);
1830                                structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1831                        } else if (value instanceof double[]) {
1832                        // @TODO other primitive array types
1833                                // @TODO convert directly probably
1834                                double[] daSrc = (double[]) value;
1835                                Double[] daTgt = new Double[daSrc.length];
1836                                for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1837                                List arrayList = Arrays.asList((Object[])daTgt);
1838                                structuredListToFilteredJson(w, (List) arrayList, jsonFormat, validKeys);
1839                        } else if (value instanceof int[]) {
1840                                int[] daSrc = (int[]) value;
1841                                Integer[] daTgt = new Integer[daSrc.length];
1842                                for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1843                                List arrayList = Arrays.asList((Object[])daTgt);
1844                                structuredListToFilteredJson(w, (List) arrayList, jsonFormat, validKeys);
1845                        } else {
1846                                throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1847                        }
1848            } else {
1849                throw new RuntimeException("Cannot translate Java object " +
1850                    value.getClass().getName() + " to javascript value");
1851            }
1852            index = index + 1;
1853            if (i.hasNext()) {
1854                w.append(',');
1855            }
1856        }
1857        w.append("]");  // removed \n
1858    }
1859
1860   public static void structuredMapToFilteredJson(Writer w, Map map, String jsonFormat, String... validKeys) throws IOException {
1861       // String s;
1862       String keyJson = null;
1863       Object value;
1864       
1865       /* List list = new ArrayList(map.keySet());
1866       Collections.sort(list, new ListComparator());
1867       */
1868       
1869       boolean isFirst = true;
1870
1871       w.append("{");
1872
1873       for (String key : validKeys) {
1874           // key = (Object) i.next();
1875           value = map.get(key);
1876           if (value != null) {
1877                   if (key instanceof String) {
1878                           keyJson = "\"" + key + "\"";
1879                   } else {
1880                           throw new IllegalArgumentException("Cannot convert key type " + key.getClass().getName() + " to javascript value");
1881                   }
1882           }
1883           if (key == null || key.equals("")) {
1884               continue; // don't allow null or empty keys, 
1885           } else if (value == null) {
1886               continue; // don't bother transferring null values to javascript
1887           } else if (value instanceof String) {
1888                   if (!isFirst) { w.append(","); }
1889                   w.append(keyJson);
1890                   w.append( ": \"");
1891                   w.append(Text.escapeJavascript((String) value));
1892                   w.append("\"");
1893           } else if (value instanceof ToJsonFormat) {
1894                   if (!isFirst) { w.append(","); }
1895                   w.append(keyJson);
1896                   w.append(": ");
1897                   w.append(((ToJsonFormat) value).toJson(jsonFormat));
1898           } else if (value instanceof ToJson) {
1899                   if (!isFirst) { w.append(","); }
1900                   w.append(keyJson);
1901                   w.append(": ");
1902                   w.append(((ToJson) value).toJson());
1903           } else if (value instanceof ToStringReturnsJson) {
1904                   if (!isFirst) { w.append(","); }
1905                   w.append(keyJson);
1906                   w.append(": ");
1907                   w.append(value.toString());
1908           } else if (value instanceof Map) {
1909                   if (!isFirst) { w.append(","); }
1910               w.append(keyJson);
1911               w.append(": ");
1912               structuredMapToFilteredJson(w, (Map) value, jsonFormat, validKeys);
1913           } else if (value instanceof List) {
1914                   if (!isFirst) { w.append(","); }
1915                   w.append(keyJson);
1916                   w.append(": ");
1917                   structuredListToFilteredJson(w, (List) value, jsonFormat, validKeys);
1918           } else if (value instanceof Number) {
1919                   if (!isFirst) { w.append(","); }
1920               w.append(keyJson);
1921               w.append(": ");
1922               w.append(value.toString());
1923           } else if (value instanceof Boolean) {
1924                   if (!isFirst) { w.append(","); }
1925                   w.append(keyJson);
1926                   w.append(": ");
1927                   w.append(value.toString());
1928           } else if (value instanceof java.util.Date) {
1929                // MS-compatible JSON encoding of Dates:
1930                // see http://weblogs.asp.net/bleroy/archive/2008/01/18/dates-and-json.aspx
1931                   if (!isFirst) { w.append(","); }
1932               w.append(keyJson);
1933               w.append(": ");
1934               w.append(Struct.toDate((java.util.Date)value, jsonFormat));
1935           } else if (value.getClass().isArray()) {
1936                   
1937                  if (value instanceof Object[]) {
1938                      List arrayList = Arrays.asList((Object[])value);
1939                      if (!isFirst) { w.append(","); }
1940                      w.append(keyJson);
1941                      w.append(": ");
1942                      structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1943                  } else if (value instanceof double[]) {
1944                  // @TODO other primitive array types
1945                  // @TODO convert directly probably
1946                          double[] daSrc = (double[]) value;
1947                          Double[] daTgt = new Double[daSrc.length];
1948                          for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1949                          List arrayList = Arrays.asList((Object[])daTgt);
1950                          if (!isFirst) { w.append(","); }
1951                          w.append(keyJson);
1952                          w.append(": ");
1953                          structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1954                  } else if (value instanceof int[]) {
1955                  // @TODO other primitive array types
1956                  // @TODO convert directly probably
1957                          int[] daSrc = (int[]) value;
1958                          Integer[] daTgt = new Integer[daSrc.length];
1959                          for (int j=0; j<daSrc.length; j++) { daTgt[j]=daSrc[j]; }
1960                          List arrayList = Arrays.asList((Object[])daTgt);
1961                          if (!isFirst) { w.append(","); }
1962                          w.append(keyJson);
1963                          w.append(": ");
1964                          structuredListToFilteredJson(w, (List)arrayList, jsonFormat, validKeys);
1965                  } else {
1966                          throw new UnsupportedOperationException("Cannot convert primitive array to JSON");
1967                  }
1968           } else {
1969               throw new RuntimeException("Cannot translate Java object " + value.getClass().getName() + " to javascript value");
1970           }
1971           isFirst = false;
1972       }
1973
1974       w.append("}");  // removed \n
1975   }
1976
1977   
1978    
1979    /** Convert a date object to it's JSON representation.
1980     * 
1981     * <p>As there's no real standard for this, a type format is used to define what kind of Dates your 
1982     * going to get on the Json side.
1983     * 
1984     * @param d a Date object
1985     * @param jsonFormat either "microsoft" or "numeric"
1986     * 
1987     * @return A date in json representation.
1988     * 
1989     * @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>
1990     */
1991    static public String toDate(Date d, String jsonFormat) {
1992                if (jsonFormat==null || jsonFormat.equals("microsoft")) {
1993                        return "\"\\/Date(" + d.getTime() +  ")\\/\"";  
1994                } else {
1995                        return String.valueOf(d.getTime());
1996                }
1997    }
1998
1999
2000
2001
2002    
2003    /** Create a structured Map out of key / value pairs. Keys must be Strings.
2004     * 
2005     * <p>So the value of <code>Struct.newStructuredMap("thing", 1L, "otherThing", "value");</code> is a map
2006     * with two entries:
2007     * <ul><li>an entry with key "thing" and value new Long(1), and 
2008     * <li>an entry  with key "otherThing" and value "value".
2009     * </ul> 
2010     * 
2011     * @param keyValuePairs a list of key / value pairs 
2012     * 
2013     * @return a Map as described above
2014     * 
2015     * @throws ClassCastException if any of the supplied keys is not a String
2016     */
2017    // looking forward to when statically defined Maps become a Java language construct
2018    static public Map<String, Object> newStructuredMap(Object... keyValuePairs) {
2019        if (keyValuePairs.length%2==1) { throw new IllegalArgumentException("keyValuePairs must have even number of elements"); }
2020        Map<String, Object> map = new HashMap();
2021        for (int i=0; i<keyValuePairs.length; i+=2) {
2022                String key = (String) keyValuePairs[i];
2023                map.put(key, keyValuePairs[i + 1]);
2024        }
2025        return map;
2026    }
2027
2028        /** Rename a structured column, as returned from a Spring JdbcTemplate query. This method will iterate
2029         * throw all rows of a table, replacing any srcColumnName keys with destColumnName
2030         * 
2031         * @param rows the table being modified
2032         * @param srcColumnName the name of the row key (column) being replaced
2033         * @param destColumnName the new name of the row key (column)
2034         */
2035        public static void renameStructuredListColumn(List<Map<String, Object>> rows, String srcColumnName, String destColumnName) {
2036                if (srcColumnName == null) { throw new NullPointerException("null srcColumnName"); }
2037                if (destColumnName == null) { throw new NullPointerException("null destColumnName"); }
2038                if (srcColumnName.equals(destColumnName)) { return; }
2039                
2040                for (int i=0; i<rows.size(); i++) {
2041                        Map<String,Object> row = rows.get(i);
2042                        if (row.containsKey(srcColumnName)) {
2043                                row.put(destColumnName, row.get(srcColumnName));
2044                                row.remove(srcColumnName);
2045                        }
2046                }
2047        }
2048
2049        /** Add a structured column containing a constant value. This method will iterate through all 
2050         * rows of a table, adding a new newColumnName key with the supplied value.
2051         * 
2052         * @param rows the table being modified
2053         * @param newColumnName the name of the row key (column) being added
2054         * @param value the value to add to the row
2055         */
2056        public static void addStructuredListColumn(List<Map<String, Object>> rows, String newColumnName, Object value) {
2057                for (int i=0; i<rows.size(); i++) {
2058                        Map<String,Object> row = rows.get(i);
2059                        row.put(newColumnName, value);
2060                }
2061        }
2062
2063    static {
2064        // NB: these used to be ConcurrentReaderHashMaps, but I didn't want to
2065        // bring concurrent.jar into the classpath.
2066        gettersCache = new ConcurrentHashMap();
2067        settersCache = new ConcurrentHashMap();
2068    }
2069}