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>=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 => ..., 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 => ..., 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 => 'stringValue' 1366 * rhinocerouseses => null 1367 * weirdObjectValue => (WeirdObjectClass) "toString() output of weirdObject" 1368 * mapValue = { 1369 * mapElement => ... 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 => ..., 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}