001/* 002 * This file is part of the JDrupes JSON utilities project. 003 * Copyright (C) 2017, 2018 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Lesser General Public License as published 007 * by the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 013 * License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.json; 020 021import com.fasterxml.jackson.core.JsonParser; 022import com.fasterxml.jackson.core.JsonToken; 023 024import java.beans.BeanInfo; 025import java.beans.ConstructorProperties; 026import java.beans.PropertyDescriptor; 027import java.beans.PropertyEditor; 028import java.io.IOException; 029import java.io.Reader; 030import java.lang.reflect.Array; 031import java.lang.reflect.Constructor; 032import java.lang.reflect.Field; 033import java.lang.reflect.InvocationTargetException; 034import java.lang.reflect.Method; 035import java.math.BigDecimal; 036import java.math.BigInteger; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Collection; 040import java.util.Comparator; 041import java.util.HashMap; 042import java.util.HashSet; 043import java.util.Map; 044import java.util.Optional; 045import java.util.Set; 046import java.util.SortedMap; 047import java.util.TreeMap; 048import java.util.function.Function; 049 050import org.jdrupes.json.JsonArray.DefaultJsonArray; 051import org.jdrupes.json.JsonObject.DefaultJsonObject; 052 053/** 054 * Decoder for converting JSON to a Java object graph. The decoding 055 * is based on the expected type passed to the decode method. 056 * 057 * The convertion rules are as follows: 058 * * If the expected type is {@link Object} and the JSON input 059 * is an array, the JSON array is converted to a {@link DefaultJsonArray}, 060 * which is an {@link ArrayList} with element type {@link Object} and 061 * some helpful accessor methods. 062 * * If the expected type implements {@link Collection} 063 * a container of the expected type is created. Its element type 064 * is again {@link Object}. The JSON input must be an array. 065 * * If the expected type is an array type, an array of the expected 066 * type with the given element type is created. The JSON input must 067 * be an array. 068 * * In all cases above, the element type is passed 069 * as expected type when decoding the members of the JSON array. 070 * * If the expected type is an {@link Object} and the JSON input 071 * is a JSON object, the input is converted to a 072 * {@link DefaultJsonObject}, which is a {@link HashMap 073 * HashMap<String,Object>} with some helpful accessor methods. 074 * * If the expected type is neither of the above, it is assumed 075 * to be a JavaBean and the JSON input must be a JSON object. 076 * The key/value pairs of the JSON input are interpreted as properties 077 * of the JavaBean and set if the values have been parsed successfully. 078 * The type of the properties are passed as expected types when 079 * parsing the values. 080 * 081 * Constructors with {@link ConstructorProperties} 082 * are used if all required values are available. Else, if no setter is 083 * available for a key/value pair, an attempt 084 * is made to gain access to a private field with the name of the 085 * key and assign the value to that field. Note thatthis will fail 086 * when using Java 9 modules unless you explicitly grant the decoder 087 * access to private fields. So defining a constructor with 088 * a {@link ConstructorProperties} annotation and all immutable 089 * properties as parameters is strongly recommended. 090 * 091 * A JSON object can have a "class" key. It must be the first key 092 * of the object. Its value is used to instantiate the Java object 093 * in which the information of the JSON object is stored. If 094 * provided, the class specified by this key/value pair overrides 095 * the class passed as expected class. It is checked, however, that the 096 * specified class is assignable to the expected class. 097 * 098 * The value specified is first matched against the aliases that 099 * have been registered with the decoder 100 * (see {@link #addAlias(Class, String)}). If no match is found, 101 * the converter set with {@link JsonBeanDecoder#setClassConverter(Function)} 102 * is used to convert the name to a class. The function defaults 103 * to {@link Class#forName(String)}. If the converter does not 104 * return a result, a {@link JsonObject} is used as container for 105 * the values provided by the JSON object. 106 */ 107public class JsonBeanDecoder extends JsonCodec { 108 109 private Map<String, Class<?>> aliases = new HashMap<>(); 110 private Function<String, Optional<Class<?>>> classConverter 111 = name -> { 112 try { 113 return Optional.ofNullable(Class.forName(name)); 114 } catch (ClassNotFoundException e) { 115 return Optional.empty(); 116 } 117 }; 118 private JsonParser parser; 119 120 @Override 121 public JsonBeanDecoder addAlias(Class<?> clazz, String alias) { 122 aliases.put(alias, clazz); 123 return this; 124 } 125 126 /** 127 * Sets the converter that maps a specified "class" to an actual Java 128 * {@link Class}. If it does not return a class, a {@link HashMap} is 129 * used to store the data of the JSON object. 130 * 131 * @param converter the converter to use 132 * @return the conversion result 133 */ 134 public JsonBeanDecoder setClassConverter( 135 Function<String, Optional<Class<?>>> converter) { 136 this.classConverter = converter; 137 return this; 138 } 139 140 /** 141 * Create a new decoder using a default {@link JsonParser}. 142 * 143 * @param in the source 144 * @return the decoder 145 */ 146 public static JsonBeanDecoder create(Reader in) { 147 try { 148 return new JsonBeanDecoder(defaultFactory().createParser(in)); 149 } catch (IOException e) { 150 throw new IllegalArgumentException(e); 151 } 152 } 153 154 /** 155 * Create a new decoder using a default parser to parse the 156 * given string. 157 * 158 * @param input the input 159 * @return the decoder 160 */ 161 public static JsonBeanDecoder create(String input) { 162 try { 163 return new JsonBeanDecoder(defaultFactory().createParser(input)); 164 } catch (IOException e) { 165 throw new IllegalArgumentException(e); 166 } 167 } 168 169 /** 170 * Create a new decoder using the given parser. 171 * 172 * @param parser the parser 173 * @return the decoder 174 */ 175 public static JsonBeanDecoder create(JsonParser parser) { 176 return new JsonBeanDecoder(parser); 177 } 178 179 public JsonBeanDecoder(JsonParser parser) { 180 if (parser == null) { 181 throw new IllegalArgumentException("Parser may not be null."); 182 } 183 this.parser = parser; 184 } 185 186 /** 187 * Read a JSON object description into a new {@link JsonObject}. 188 * 189 * @return the object 190 * @throws JsonDecodeException 191 */ 192 public JsonObject readObject() throws JsonDecodeException { 193 try { 194 return readValue(DefaultJsonObject.class); 195 } catch (IOException e) { 196 throw new JsonDecodeException(e); 197 } 198 } 199 200 /** 201 * Read a JSON object description into a new object of the 202 * expected type. The result may have a type derived from 203 * the expected type if the JSON read has a `class` key. 204 * 205 * @param expected the expected type 206 * @return the result 207 * @throws JsonDecodeException 208 */ 209 public <T> T readObject(Class<T> expected) throws JsonDecodeException { 210 try { 211 return readValue(expected); 212 } catch (IOException e) { 213 throw new JsonDecodeException(e); 214 } 215 } 216 217 /** 218 * Read a JSON array description into a new array of the 219 * expected type. 220 * 221 * @param <T> the generic type 222 * @param expected the expected type 223 * @return the result 224 * @throws JsonDecodeException 225 */ 226 public <T> T readArray(Class<T> expected) throws JsonDecodeException { 227 try { 228 return readValue(expected); 229 } catch (IOException e) { 230 throw new JsonDecodeException(e); 231 } 232 } 233 234 private static final Object END_VALUE = new Object(); 235 236 @SuppressWarnings("unchecked") 237 private <T> T readValue(Class<T> expected) 238 throws JsonDecodeException, IOException { 239 JsonToken token = parser.nextToken(); 240 if (token == null) { 241 return null; 242 } 243 switch (token) { 244 case END_ARRAY: 245 case END_OBJECT: 246 return (T) END_VALUE; 247 case VALUE_NULL: 248 return null; 249 case VALUE_FALSE: 250 return (T) Boolean.FALSE; 251 case VALUE_TRUE: 252 return (T) Boolean.TRUE; 253 case VALUE_STRING: 254 PropertyEditor propertyEditor = findPropertyEditor(expected); 255 if (propertyEditor != null) { 256 propertyEditor.setAsText(parser.getText()); 257 return (T) propertyEditor.getValue(); 258 } 259 if (Enum.class.isAssignableFrom(expected)) { 260 @SuppressWarnings("rawtypes") 261 Class<Enum> enumClass = (Class<Enum>) expected; 262 return (T) Enum.valueOf(enumClass, parser.getText()); 263 } 264 // fall through 265 case FIELD_NAME: 266 return (T) parser.getText(); 267 case START_ARRAY: 268 if (expected.isArray() 269 || Collection.class.isAssignableFrom(expected) 270 || expected.equals(Object.class)) { 271 return (T) readArrayValue(expected); 272 } 273 throw new JsonDecodeException(parser.getCurrentLocation() 274 + ": Encountered unexpected array."); 275 case START_OBJECT: 276 return readObjectValue(expected); 277 default: 278 if (token.isScalarValue()) { 279 return readNumber(expected); 280 } 281 throw new JsonDecodeException(parser.getCurrentLocation() 282 + ": Unexpected event."); 283 } 284 } 285 286 @SuppressWarnings("unchecked") 287 private <T> T readNumber(Class<T> expected) throws IOException { 288 if (expected.equals(Byte.class) 289 || expected.equals(Byte.TYPE)) { 290 return (T) Byte.valueOf((byte) parser.getValueAsInt()); 291 } 292 if (expected.equals(Short.class) 293 || expected.equals(Short.TYPE)) { 294 return (T) Short.valueOf((short) parser.getValueAsInt()); 295 } 296 if (expected.equals(Integer.class) 297 || expected.equals(Integer.TYPE)) { 298 return (T) Integer.valueOf(parser.getValueAsInt()); 299 } 300 if (expected.equals(BigInteger.class)) { 301 return (T) parser.getBigIntegerValue(); 302 } 303 if (expected.equals(BigDecimal.class)) { 304 return (T) parser.getDecimalValue(); 305 } 306 if (expected.equals(Float.class) 307 || expected.equals(Float.TYPE)) { 308 return (T) Float.valueOf((float) parser.getValueAsDouble()); 309 } 310 if (expected.equals(Long.class) 311 || expected.equals(Long.TYPE) 312 || parser.currentToken() == JsonToken.VALUE_NUMBER_INT) { 313 return (T) Long.valueOf(parser.getValueAsLong()); 314 } 315 return (T) Double.valueOf(parser.getValueAsDouble()); 316 } 317 318 @SuppressWarnings("unchecked") 319 private <T> Object readArrayValue(Class<T> arrayType) 320 throws JsonDecodeException, IOException { 321 Collection<T> items = createCollection(arrayType); 322 Class<?> itemType = Object.class; 323 if (arrayType.isArray()) { 324 itemType = arrayType.getComponentType(); 325 } 326 while (true) { 327 T item = (T) readValue(itemType); 328 if (item == END_VALUE) { 329 break; 330 } 331 items.add(item); 332 } 333 if (!arrayType.isArray()) { 334 return items; 335 } 336 Object result = Array.newInstance(itemType, items.size()); 337 int index = 0; 338 for (Object o : items) { 339 Array.set(result, index++, o); 340 } 341 return result; 342 } 343 344 private <T> Collection<T> createCollection(Class<T> arrayType) 345 throws JsonDecodeException { 346 if (!Collection.class.isAssignableFrom(arrayType)) { 347 @SuppressWarnings("unchecked") 348 Collection<T> result = (Collection<T>) JsonArray.create(); 349 return result; 350 } 351 if (arrayType.isInterface()) { 352 // This is how things should be: interface type 353 if (Set.class.isAssignableFrom(arrayType)) { 354 return new HashSet<>(); 355 } 356 @SuppressWarnings("unchecked") 357 Collection<T> result = (Collection<T>) JsonArray.create(); 358 return result; 359 } 360 // Implementation type, we'll try our best 361 try { 362 @SuppressWarnings("unchecked") 363 Collection<T> result = (Collection<T>) arrayType 364 .getDeclaredConstructor().newInstance(); 365 return result; 366 } catch (InstantiationException | IllegalAccessException 367 | IllegalArgumentException | InvocationTargetException 368 | NoSuchMethodException | SecurityException e) { 369 throw new JsonDecodeException(parser.getCurrentLocation() 370 + ": Cannot create " + arrayType.getName(), e); 371 } 372 } 373 374 private <T> T readObjectValue(Class<T> expected) 375 throws JsonDecodeException, IOException { 376 JsonToken prefetched = parser.nextToken(); 377 if (!prefetched.equals(JsonToken.FIELD_NAME) 378 && !prefetched.equals(JsonToken.END_OBJECT)) { 379 throw new JsonDecodeException(parser.getCurrentLocation() 380 + ": Unexpected Json event " + prefetched); 381 } 382 Class<?> actualCls = expected; 383 if (prefetched.equals(JsonToken.FIELD_NAME)) { 384 String key = parser.getText(); 385 if ("class".equals(key)) { 386 prefetched = null; // Now it's consumed 387 parser.nextToken(); 388 String provided = parser.getText(); 389 if (aliases.containsKey(provided)) { 390 actualCls = aliases.get(provided); 391 } else { 392 actualCls = classConverter.apply(provided) 393 .orElse(DefaultJsonObject.class); 394 } 395 } 396 } 397 if (!expected.isAssignableFrom(actualCls)) { 398 throw new JsonDecodeException(parser.getCurrentLocation() 399 + ": Expected " + expected.getName() 400 + " found " + actualCls.getName()); 401 } 402 if (actualCls.equals(Object.class)) { 403 actualCls = DefaultJsonObject.class; 404 } 405 if (Map.class.isAssignableFrom(actualCls)) { 406 @SuppressWarnings("unchecked") 407 Map<String, Object> map = createMapInstance( 408 (Class<Map<String, Object>>) actualCls); 409 objectIntoMap(map, prefetched); 410 @SuppressWarnings("unchecked") 411 T result = (T) map; 412 return result; 413 } 414 @SuppressWarnings("unchecked") 415 Class<T> beanCls = (Class<T>) actualCls; 416 return objectToBean(beanCls, prefetched); 417 } 418 419 private <M extends Map<String, Object>> M createMapInstance(Class<M> mapCls) 420 throws JsonDecodeException { 421 try { 422 return (M) mapCls.getDeclaredConstructor().newInstance(); 423 } catch (InstantiationException 424 | IllegalAccessException | IllegalArgumentException 425 | InvocationTargetException | NoSuchMethodException 426 | SecurityException e) { 427 throw new JsonDecodeException(parser.getCurrentLocation() 428 + ": Cannot create " + mapCls.getName(), e); 429 } 430 } 431 432 private void objectIntoMap(Map<String, Object> result, JsonToken prefetched) 433 throws JsonDecodeException, IOException { 434 whileLoop: while (true) { 435 JsonToken event 436 = prefetched != null ? prefetched : parser.nextToken(); 437 prefetched = null; // Consumed. 438 switch (event) { 439 case END_OBJECT: 440 break whileLoop; 441 442 case FIELD_NAME: 443 String key = parser.getText(); 444 Object value = readValue(Object.class); 445 result.put(key, value); 446 break; 447 448 default: 449 throw new JsonDecodeException(parser.getCurrentLocation() 450 + ": Unexpected Json event " + event); 451 } 452 } 453 } 454 455 private <T> T objectToBean(Class<T> beanCls, JsonToken prefetched) 456 throws JsonDecodeException, IOException { 457 Map<String, PropertyDescriptor> beanProps = new HashMap<>(); 458 BeanInfo beanInfo = findBeanInfo(beanCls); 459 if (beanInfo == null) { 460 throw new JsonDecodeException(parser.getCurrentLocation() 461 + ": Cannot introspect " + beanCls); 462 } 463 for (PropertyDescriptor p : beanInfo.getPropertyDescriptors()) { 464 beanProps.put(p.getName(), p); 465 } 466 467 // Get properties as map first. 468 Map<String, Object> propsMap = parseProperties(beanProps, prefetched); 469 470 // Prepare result, using constructor with parameters if available. 471 T result = createBean(beanCls, propsMap); 472 473 // Set (remaining) properties. 474 for (Map.Entry<String, ?> e : propsMap.entrySet()) { 475 PropertyDescriptor property = beanProps.get(e.getKey()); 476 if (property == null) { 477 throw new JsonDecodeException(parser.getCurrentLocation() 478 + ": No bean property for key " + e.getKey()); 479 } 480 setProperty(result, property, e.getValue()); 481 } 482 return result; 483 } 484 485 private <T> T createBean(Class<T> beanCls, Map<String, Object> propsMap) 486 throws JsonDecodeException { 487 try { 488 SortedMap<ConstructorProperties, Constructor<T>> cons 489 = new TreeMap<>(Comparator.comparingInt( 490 (ConstructorProperties cp) -> cp.value().length) 491 .reversed()); 492 for (Constructor<?> c : beanCls.getConstructors()) { 493 ConstructorProperties[] allCps = c.getAnnotationsByType( 494 ConstructorProperties.class); 495 if (allCps.length > 0) { 496 @SuppressWarnings("unchecked") 497 Constructor<T> beanConstructor = (Constructor<T>) c; 498 cons.put(allCps[0], beanConstructor); 499 } 500 } 501 for (Map.Entry<ConstructorProperties, Constructor<T>> e : cons 502 .entrySet()) { 503 String[] conProps = e.getKey().value(); 504 if (propsMap.keySet().containsAll(Arrays.asList(conProps))) { 505 Object[] args = new Object[conProps.length]; 506 for (int i = 0; i < conProps.length; i++) { 507 args[i] = propsMap.remove(conProps[i]); 508 } 509 T result = e.getValue().newInstance(args); 510 return result; 511 } 512 } 513 514 return beanCls.getDeclaredConstructor().newInstance(); 515 } catch (InstantiationException | IllegalAccessException 516 | IllegalArgumentException | InvocationTargetException 517 | NoSuchMethodException | SecurityException e) { 518 throw new JsonDecodeException(parser.getCurrentLocation() 519 + ": Cannot create " + beanCls.getName(), e); 520 } 521 } 522 523 private Map<String, Object> parseProperties( 524 Map<String, PropertyDescriptor> beanProps, JsonToken prefetched) 525 throws JsonDecodeException, IOException { 526 Map<String, Object> map = new HashMap<>(); 527 whileLoop: while (true) { 528 JsonToken event 529 = prefetched != null ? prefetched : parser.nextToken(); 530 prefetched = null; // Consumed. 531 switch (event) { 532 case END_OBJECT: 533 break whileLoop; 534 535 case FIELD_NAME: 536 String key = parser.getText(); 537 PropertyDescriptor property = beanProps.get(key); 538 if (property == null) { 539 throw new JsonDecodeException(parser.getCurrentLocation() 540 + ": No bean property for key " + key); 541 } 542 Object value = readValue(property.getPropertyType()); 543 map.put(key, value); 544 break; 545 546 default: 547 throw new JsonDecodeException(parser.getCurrentLocation() 548 + ": Unexpected Json event " + event); 549 } 550 } 551 return map; 552 } 553 554 @SuppressWarnings("deprecation") 555 private <T> void setProperty(T obj, PropertyDescriptor property, 556 Object value) throws JsonDecodeException { 557 try { 558 Method writeMethod = property.getWriteMethod(); 559 if (writeMethod != null) { 560 writeMethod.invoke(obj, value); 561 return; 562 } 563 Field propField = findField(obj.getClass(), property.getName()); 564 if (!propField.isAccessible()) { 565 propField.setAccessible(true); 566 } 567 propField.set(obj, value); 568 } catch (IllegalAccessException | IllegalArgumentException 569 | InvocationTargetException | NoSuchFieldException e) { 570 throw new JsonDecodeException(parser.getCurrentLocation() 571 + ": Cannot write property " + property.getName(), e); 572 } 573 } 574 575 private Field findField(Class<?> cls, String fieldName) 576 throws NoSuchFieldException { 577 if (cls.equals(Object.class)) { 578 throw new NoSuchFieldException(); 579 } 580 try { 581 return cls.getDeclaredField(fieldName); 582 } catch (NoSuchFieldException e) { 583 return findField(cls.getSuperclass(), fieldName); 584 } 585 } 586}