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&lt;String,Object&gt;} 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}