# JavaTypeSerializer.java

    /*
    ** Module   : JavaTypeSerializer.java
    ** Abstract : Base class to parse and serialize various Java types (including native types, POJO, record, etc).
    **
    ** Copyright (c) 2022-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20220323 Created initial version.
    **     TJD 20220504 Java 11 compatibility minor changes
    ** 002 SBI 20221005 Fixed getSerializerInt for nested generic parameters. For example, ArrayList<Map<String, Object>>.
    **     TJD 20220504 Java 11 compatibility minor changes
    ** 003 DDF 20230620 Replaced static initialization of values from the directory configuration with
    **                  a method called at server bootstrap.
    ** 004 TJD 20240123 Java 17 compatibility updates
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.rest.serializers;

    import java.lang.reflect.*;
    import java.math.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.function.*;

    import org.reflections.*;

    import com.fasterxml.jackson.core.*;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.node.*;
    import com.goldencode.asm.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.rest.*;
    import com.goldencode.p2j.util.*;

    /**
     * The base class to be implemented by any custom or default serializer, to be used when invoking REST services
     * which are performed as a direct Java call (on the appserver agent).
     * <p>
     * All public serializers defined in the <code>com.goldencode.p2j.rest.serializers</code> package are resolved
     * and registered.  Custom serializers can be specified by setting <code>rest/custom-java-serializers</code>
     * to a Java package holding these custom serializers, in the application's jar.
     * <p>
     * All mutable instances created by the serializer's {@link #fromJson} method can be expected to have their
     * changes reflected back on the caller, regardless if the argument is OUTPUT or not.  It is the caller's
     * responsibility to ensure that only OUTPUT, INPUT-OUTPUT or RETURN arguments are being processed.
     *
     * @param    <T>
     *           The target type of this serializer.  It can be a super-class or interface, a concrete type
     *           implementing a certain interface or class.  When resolving the serializer, the one closest to
     *           the object's type will be resolved.  For example, if the object's type is a DMO (like Book),
     *           and exists both a {@link RecordSerializer} and a custom serializer for this Book DMO, then the
     *           custom serializer will be resolved.
     */
    @SuppressWarnings("unchecked")
    public abstract class JavaTypeSerializer<T>
    {
       /** The serializer for {@link String} instances. */
       protected static JavaTypeSerializer STRING_SERIALIZER;

       /** The serializer for {@link Boolean} instances. */
       protected static JavaTypeSerializer BOOLEAN_SERIALIZER;

       /** The serializer for {@link BigDecimal} instances. */
       protected static JavaTypeSerializer BIGDECIMAL_SERIALIZER;

       /** The serializer for {@link BigInteger} instances. */
       protected static JavaTypeSerializer BIGINTEGER_SERIALIZER;

       /** The serializer for {@link Short} instances. */
       protected static JavaTypeSerializer SHORT_SERIALIZER;

       /** The serializer for {@link Integer} instances. */
       protected static JavaTypeSerializer INTEGER_SERIALIZER;

       /** The serializer for {@link Long} instances. */
       protected static JavaTypeSerializer LONG_SERIALIZER;

       /** The serializer for {@link Float} instances. */
       protected static JavaTypeSerializer FLOAT_SERIALIZER;

       /** The serializer for {@link Double} instances. */
       protected static JavaTypeSerializer DOUBLE_SERIALIZER;

       /** The serializer for Java byte arrays (byte[]). */
       protected static JavaTypeSerializer BYTEARRAY_SERIALIZER;

       /**
        * The mapping of a type to its serializer.  The type follows the convention of {@link Type#getTypeName()}.
        */
       private static final Map<String, JavaTypeSerializer> SERIALIZERS = new ConcurrentHashMap<>();

       /** The primitive type serializers. */
       private static final Map<String, Class<?>> PRIMITIVE_TYPES;

       static
       {
          Map<String, Class<?>> types = new HashMap<>();
          types.put(boolean.class.getTypeName(), boolean.class);
          types.put(byte.class.getTypeName(),    byte.class);
          types.put(char.class.getTypeName(),    char.class);
          types.put(short.class.getTypeName(),   short.class);
          types.put(int.class.getTypeName(),     int.class);
          types.put(long.class.getTypeName(),    long.class);
          types.put(float.class.getTypeName(),   float.class);
          types.put(double.class.getTypeName(),  double.class);
          PRIMITIVE_TYPES = Collections.unmodifiableMap(types);
       }

       /** Used to parse the JSON payload. */
       protected final ThreadLocal<ObjectMapper> mapper = new ThreadLocal<ObjectMapper>()
       {
          protected ObjectMapper initialValue()
          {
             return new ObjectMapper();
          };
       };

       /** The serializer's type. */
       private final Class<T> type;

       /**
        * Initialize this serializer with the given type.
        *
        * @param    type
        *           The target type.
        */
       public JavaTypeSerializer(Class<T> type)
       {
          this.type = type;
       }

       /**
        * Method called at server bootstrap that initializes values from the directory
        * configuration. Until this method is called, default values are used.
        */
       public static void bootstrap()
       {
          loadSerializers("com.goldencode.p2j.rest.serializers");

          String customPackage = Utils.getDirectoryNodeString(null,
                                                              "rest/custom-java-serializers",
                                                              null,
                                                              false);
          if (customPackage != null)
          {
             loadSerializers(customPackage);
          }

          STRING_SERIALIZER = getSerializer(String.class.getTypeName());
          BOOLEAN_SERIALIZER = getSerializer(Boolean.class.getTypeName());
          BIGDECIMAL_SERIALIZER = getSerializer(BigDecimal.class.getTypeName());
          BIGINTEGER_SERIALIZER = getSerializer(BigInteger.class.getTypeName());
          SHORT_SERIALIZER = getSerializer(Short.class.getTypeName());
          INTEGER_SERIALIZER = getSerializer(Integer.class.getTypeName());
          LONG_SERIALIZER = getSerializer(Long.class.getTypeName());
          FLOAT_SERIALIZER = getSerializer(Float.class.getTypeName());
          DOUBLE_SERIALIZER = getSerializer(Double.class.getTypeName());
          BYTEARRAY_SERIALIZER = getSerializer(byte[].class.getTypeName());
       }

       /**
        * Create an initial (default) instance for this serializer.
        * <p>
        * Can be null for non-mutable instances.
        *
        * @param    definitionType
        *           The type as it appears at the parameter's definition.
        *
        * @return   <code>null</code> for immutable types, a Java native value for native types or a new instance,
        *           for mutable types.
        *
        * @throws   InstantiationException
        * @throws   IllegalAccessException
        */
       public abstract T initialize(Class<? extends T> definitionType)
       throws ReflectiveOperationException;

       /**
        * Parse the string JSON representation and assign it to the given argument.  If null or not mutable, a
        * new instance will be created.
        *
        * @param    arg
        *           The argument (obtained via {@link #initialize}.
        * @param    sval
        *           The string JSON representation of this argument.
        *
        * @return   The argument instance.
        *
        * @throws   RequestArgumentError
        *           If the argument can't be parsed.
        */
       public abstract T fromJson(T arg, String sval)
       throws RequestArgumentError;

       /**
        * Serialize the given instance to JSON.
        *
        * @param    val
        *           The instance to serialize.
        *
        * @return   The JSON representation of this instance.
        */
       public abstract JsonNode toJson(T val);

       /**
        * Given a {@link Type#getTypeName()} representation of a Java type, resolve it to a Java {@link Class}.
        * <p>
        * If this is a native type name, then the {@link #PRIMITIVE_TYPES} will resolve it.
        * <p>
        * If there is a serializer registered for this type in {@link #SERIALIZERS}, the the serializer's
        * {@link #type} is returned.
        * <p>
        * Otherwise, it is assumed this is a Java type name, and will be resolved either by {@link Class#forName}
        * or via the {@link AsmClassLoader}, in case of DMO types.
        *
        * @param    stype
        *           The string representation of this type.
        *
        * @return   See above.
        *
        * @throws   ClassNotFoundException
        *           If the type can't be resolved.
        */
       public static Class<?> resolveJavaType(String stype)
       throws ClassNotFoundException
       {
          int idx = stype.indexOf('<');
          String type = (idx < 0 ? stype : stype.substring(0, idx));
          Class<?> res = PRIMITIVE_TYPES.get(type);

          if (res != null)
          {
             return res;
          }

          JavaTypeSerializer serializer = SERIALIZERS.get(stype);
          if (serializer != null)
          {
             return serializer.getType();
          }

          try
          {
             return Class.forName(type);
          }
          catch (ClassNotFoundException exc)
          {
             return AsmClassLoader.getInstance().findClass(type);
          }
       }

       /**
        * Get the serializer associated with the given type.
        *
        * @param    stype
        *           The string representation of this type (including any generic parameters), as returned by
        *           {@link Type#getTypeName()}.
        *
        * @return   See above.
        */
       public static JavaTypeSerializer getSerializer(String stype)
       {
          return getSerializer(stype, SourceNameMapper.NO_EXTENT);
       }

       /**
        * Get the serializer associated with the given type.
        * <p>
        * If the serializer is resolved to an {@link ArraySerializer}, a new instance is created and the specified
        * extent is set.
        *
        * @param    stype
        *           The string representation of this type (including any generic parameters), as returned by
        *           {@link Type#getTypeName()}.
        * @param    extent
        *           In case of arrays, a positive value indicating the array's length.
        *
        * @return   See above.
        */
       public static JavaTypeSerializer getSerializer(String stype, int extent)
       {
          JavaTypeSerializer serializer = getSerializerInt(stype, extent);
          if (serializer instanceof ArraySerializer)
          {
             serializer = new ArraySerializer((ArraySerializer) serializer, extent);
          }

          return serializer;
       }

       /**
        * Get the serializer associated with the given type.
        *
        * @param    stype
        *           The string representation of this type (including any generic parameters), as returned by
        *           {@link Type#getTypeName()}.
        * @param    extent
        *           In case of arrays, a positive value indicating the array's length.
        *
        * @return   See above.
        */
       private static JavaTypeSerializer getSerializerInt(String stype, int extent)
       {
          JavaTypeSerializer serializer = SERIALIZERS.get(stype);
          if (serializer != null)
          {
             return serializer;
          }

          if ("byte[]".equals(stype))
          {
             serializer = new ByteArraySerializer();
             SERIALIZERS.put(stype, serializer);
             return serializer;
          }

          int idx = stype.indexOf('<');
          String type = (idx < 0 ? stype : stype.substring(0, idx));
          serializer = SERIALIZERS.get(type);

          if (serializer == null)
          {
             Class cls;
             try
             {
                // check for arrays
                if (type.indexOf('[') > 0)
                {
                   int dim = 0;
                   String tarr = type;
                   while (tarr.indexOf('[') > 0)
                   {
                      dim = dim + 1;
                      tarr = tarr.substring(tarr.indexOf('[') + 1);
                   }

                   String arrType = type.substring(0, type.indexOf('['));
                   Class arrClazz = resolveJavaType(arrType);
                   int[] dimensions = new int[dim];
                   arrClazz = Array.newInstance(arrClazz, dimensions).getClass();
                   for (int i = 0; i < dim - 1; i++)
                   {
                      arrType += "[]";
                   }

                   JavaTypeSerializer ctype = getSerializer(arrType);
                   serializer = new ArraySerializer(arrClazz, ctype);

                   SERIALIZERS.put(stype, serializer);
                   return serializer;
                }

                cls = resolveJavaType(type);
             }
             catch (ClassNotFoundException e)
             {
                // should never happen, leave it for debugging purposes
                throw new RuntimeException(e);
             }

             if (cls.isArray())
             {
                JavaTypeSerializer ctype = getSerializer(cls.getComponentType().getName());
                serializer = new ArraySerializer(cls, ctype);
                SERIALIZERS.put(stype, serializer);
                return serializer;
             }

             // if not yet registered, look into all super-interfaces and all super-classes and find if
             // anything is registered for it. this allows to have a e.g. java.util.Set serializer for any
             // Set implementation.
             Set<JavaTypeSerializer> found = new HashSet<>();
             Utils.dfsClassHierarchy(cls, (scls) ->
             {
                JavaTypeSerializer ser = SERIALIZERS.get(scls.getName());
                if (ser != null)
                {
                   found.add(ser);
                }
             });

             if (found.size() == 1)
             {
                serializer = found.iterator().next();
             }
             else
             {
                // look for the most specific serializer, which doesn't have a serializer for a super-class or
                // implemented interface already registered
                List<JavaTypeSerializer> candidates = new ArrayList<>();
                for (JavaTypeSerializer ser1 : found)
                {
                   boolean ok = true;
                   for (JavaTypeSerializer ser2 : found)
                   {
                      if (ser1 != ser2 && ser1.getType().isAssignableFrom(ser2.getType()))
                      {
                         ok = false;
                         break;
                      }
                   }

                   if (ok)
                   {
                      candidates.add(ser1);
                   }
                }

                if (candidates.size() != 1)
                {
                   throw new IllegalStateException("Could not determine the serializer for " + cls.toString() +
                                                   " from: " + found.toString());
                }

                serializer = candidates.get(0);
             }
          }

          if (serializer == null)
          {
             throw new NullPointerException("No serializer defined for " + type);
          }

          if (idx < 0)
          {
             // non-parameterized type
             return serializer;
          }

          // create a new one with the parameters, and register it.
          try
          {
             serializer = serializer.getClass().getDeclaredConstructor().newInstance();
          }
          catch (ReflectiveOperationException e)
          {
             // ignore, can't happen
          }

          List<JavaTypeSerializer> parameters = new ArrayList<>();
          String sparams = stype.substring(idx + 1).trim();
          sparams = sparams.substring(0, sparams.lastIndexOf('>')).trim();

          // split by comma and get all the parameters
          idx = sparams.indexOf(',');
          int scan = 0;
          int balance = 0;
          while (idx > 0)
          {
             // parse generic parameter types
             for(int i = scan; i < sparams.length() && i < idx; i++)
             {
                if (sparams.charAt(i) == '<')
                {
                   balance++;
                }
                else if (sparams.charAt(i) == '>')
                {
                   balance--;
                }
             }
             if (balance == 0)
             {
                String ptype = sparams.substring(0, idx).trim();
                parameters.add(getSerializer(ptype));
                sparams = sparams.substring(idx + 1).trim();
                scan = 0;
             }
             else
             {
                scan = idx + 1;
             }
             idx = sparams.indexOf(',', scan);
          }
          // add the last one
          parameters.add(getSerializer(sparams));

          JavaTypeSerializer[] params = parameters.toArray(new JavaTypeSerializer[parameters.size()]);
          ((ParameterizedTypeSerializer) serializer).setParameters(params);

          SERIALIZERS.put(stype, serializer);

          return serializer;
       }

       /**
        * Get a serializer based on the JSON node's type.
        * <p>
        * If one can't be resolved, <code>null</code> is returned.
        *
        * @param    node
        *           The JSON node.
        *
        * @return   See above.
        */
       private static JavaTypeSerializer getSerializer(JsonNode node)
       {
          if (node.isBigDecimal())
          {
             return BIGDECIMAL_SERIALIZER;
          }
          else if (node.isBigInteger())
          {
             return BIGINTEGER_SERIALIZER;
          }
          else if (node.isBinary())
          {
             return BYTEARRAY_SERIALIZER;
          }
          else if (node.isBoolean())
          {
             return BOOLEAN_SERIALIZER;
          }
          else if (node.isDouble())
          {
             return DOUBLE_SERIALIZER;
          }
          else if (node.isFloat())
          {
             return FLOAT_SERIALIZER;
          }
          else if (node.isFloatingPointNumber())
          {
             return FLOAT_SERIALIZER;
          }
          else if (node.isInt())
          {
             return INTEGER_SERIALIZER;
          }
          else if (node.isIntegralNumber())
          {
             return INTEGER_SERIALIZER;
          }
          else if (node.isLong())
          {
             return LONG_SERIALIZER;
          }
          else if (node.isNumber())
          {
             return DOUBLE_SERIALIZER;
          }
          else if (node.isShort())
          {
             return SHORT_SERIALIZER;
          }
          else if (node.isTextual())
          {
             return STRING_SERIALIZER;
          }

          return null;
       }

       /**
        * Load all {@link JavaTypeSerializer} implementation in the given package.
        * <p>
        * Only non-abstract and public classes are resolved.
        *
        * @param    pkg
        *           The package where the serializers reside.
        */
       private static void loadSerializers(String pkg)
       {
          Reflections reflections = new Reflections(pkg);
          Set<Class<? extends JavaTypeSerializer>> def = reflections.getSubTypesOf(JavaTypeSerializer.class);
          for (Class<? extends JavaTypeSerializer> type : def)
          {
             if (Modifier.isAbstract(type.getModifiers()) || !Modifier.isPublic(type.getModifiers()))
             {
                continue;
             }

             try
             {
                JavaTypeSerializer ser;
                try
                {
                   ser = type.getConstructor().newInstance();
                }
                catch (IllegalArgumentException |
                       InvocationTargetException |
                       NoSuchMethodException |
                       SecurityException e)
                {
                   throw new IllegalStateException("Serializer " + type.getName() +
                                                   " must have a public default constructor.", e);
                }

                JavaTypeSerializer old = SERIALIZERS.put(ser.getType().getTypeName(), ser);
                if (old != null)
                {
                   throw new IllegalStateException("More than one serializer registered for " + ser.getType());
                }
             }
             catch (InstantiationException |
                    IllegalAccessException e)
             {
                throw new RuntimeException(e);
             }
          }
       }

       /**
        * Get the serializer's type.
        *
        * @return   See above.
        */
       public Class<T> getType()
       {
          return type;
       }

       /**
        * Parse the JSON node and assign it to the given argument.  If null or not mutable, a new instance will be
        * created.
        *
        * @param    arg
        *           The argument (obtained via {@link #initialize}.
        * @param    node
        *           The JSON node for this argument.
        *
        * @return   The argument instance.
        *
        * @throws   RequestArgumentError
        *           If the argument can't be parsed.
        */
       protected T fromJson(T arg, JsonNode node)
       throws RequestArgumentError
       {
          return fromJson(arg, node.isTextual() ? node.asText() : node.toString());
       }

       /**
        * Create a new {@link DataModelObject} of the given type.
        *
        * @param    definitionType
        *           The DMO interface.
        *
        * @return   A new {@link Record} instance.
        *
        * @throws InstantiationException
        * @throws IllegalAccessException
        */
       protected Record newRecord(Class<? extends DataModelObject> definitionType)
       throws ReflectiveOperationException
       {
          DmoMeta dmoInfo = DmoMetadataManager.registerDmo((Class<? extends DataModelObject>) definitionType);
          Record record = dmoInfo.getImplementationClass().getDeclaredConstructor().newInstance();
          record.initialize(true);

          return record;
       }

       /**
        * Create a new instance of the given type.  If the type is abstract, then the supplier will be used to
        * create an instance.
        *
        * @param    definitionType
        *           The parameter's definition type.
        * @param    def
        *           The supplier of a default instance of this type.
        *
        * @return   See above.
        *
        * @throws InstantiationException
        * @throws IllegalAccessException
        */
       protected T newInstance(Class<? extends T> definitionType, Supplier<? extends T> def)
       throws ReflectiveOperationException
       {
          if (Modifier.isAbstract(definitionType.getModifiers()))
          {
             return def.get();
          }

          return definitionType.getDeclaredConstructor().newInstance();
       }

       /**
        * Read a JSON array and load it in a Java list.
        *
        * @param    component
        *           The serializer for this array.
        * @param    ctype
        *           The component's type.
        * @param    sval
        *           The string JSON representation of this array.
        *
        * @return   The array, read as a list.
        *
        * @throws   RequestArgumentError
        *           If the array could not be parsed.
        */
       protected List<Object> readArray(JavaTypeSerializer component, Class<?> ctype, String sval)
       throws RequestArgumentError
       {
          List<Object> res = new ArrayList<>();
          readArray(component, ctype, sval, res);
          return res;
       }

       /**
        * Given a JSON array represented as a string, load its values in a Java collection.
        *
        * @param    component
        *           The serializer for the array's values.  If <code>null</code>, it will be resolved from the
        *           value's type.
        * @param    sval
        *           The string representation of this JSON array.
        * @param    type
        *           The collection's concrete type.
        *           value's type.
        *
        * @return   See above.
        *
        * @throws   RequestArgumentError
        *           If the key or value can't be parsed.
        */
       protected Collection readArray(JavaTypeSerializer component, String sval, Class<? extends Collection> type)
       throws RequestArgumentError
       {
          try
          {
             Collection res = type.getDeclaredConstructor().newInstance();
             Class<?> ctype = component == null ? null : component.getType();
             readArray(component, ctype, sval, res);
             return res;
          }
          catch (ReflectiveOperationException e)
          {
             throw new RequestArgumentError("Could not parse " + sval, e);
          }
       }

       /**
        * Given a JSON array represented as a string, load its values in a Java collection.
        *
        * @param    component
        *           The serializer for the array's values.  If <code>null</code>, it will be resolved from the
        *           value's type.
        * @param    ctype
        *           The array's component concrete type.  If <code>null</code> it will be resolved from the
        *           value's type.
        * @param    sval
        *           The string representation of this JSON array.
        * @param    target
        *           The target collection where to load the array.
        *
        * @throws   RequestArgumentError
        *           If the key or value can't be parsed.
        */
       protected void readArray(JavaTypeSerializer component, Class<?> ctype, String sval, Collection target)
       throws RequestArgumentError
       {
          try
          {
             ArrayNode arr = (ArrayNode) mapper.get().readTree(sval);

             for (int i = 0; i < arr.size(); i++)
             {
                JsonNode node = arr.get(i);
                Object val;

                JavaTypeSerializer serializer = component == null ? getSerializer(node) : component;

                if (serializer != null)
                {
                   val = serializer.initialize(ctype == null ? serializer.getType() : ctype);
                   val = serializer.fromJson(val, node);
                }
                else
                {
                   // if the component type is not known, always assume String
                   val = node.isNull() ? null : node.isTextual() ? node.asText() : node.toString();
                }

                target.add(val);
             }
          }
          catch (JsonProcessingException      |
                 ReflectiveOperationException e)
          {
             throw new RequestArgumentError("Could not parse " + sval, e);
          }
       }

       /**
        * Given a JSON object represented as a string, load its keys and values in a map.
        *
        * @param    pkey
        *           The serializer for the keys.  If <code>null</code>, it will be resolved from the keys's type.
        * @param    pval
        *           The serializer for the values.  If <code>null</code>, it will be resolved from the value's type.
        * @param    sval
        *           The string representation of this JSON object.
        * @param    type
        *           The map's concrete type.
        *
        * @return   See above.
        *
        * @throws   RequestArgumentError
        *           If the key or value can't be parsed.
        */
       protected Map readMap(JavaTypeSerializer   pkey,
                             JavaTypeSerializer   pval,
                             String               sval,
                             Class<? extends Map> type)
       throws RequestArgumentError
       {
          try
          {
             Map res = type.getDeclaredConstructor().newInstance();

             ObjectNode obj = (ObjectNode) mapper.get().readTree(sval);

             Iterator<Map.Entry<String, JsonNode>> iter = obj.fields();
             while (iter.hasNext())
             {
                Map.Entry<String, JsonNode> field = iter.next();
                String key = field.getKey();
                JsonNode node = field.getValue();

                JavaTypeSerializer serializer = pval == null ? getSerializer(node) : pval;

                Object val;
                if (serializer != null)
                {
                   val = serializer.initialize(serializer.getType());
                   val = serializer.fromJson(val, node);
                }
                else
                {
                   // if the component type is not known, always assume String
                   val = node.isNull() ? null : node.isTextual() ? node.asText() : node.toString();
                }

                Object okey = key;
                if (pkey != null)
                {
                   // serialize on the expected type
                   okey = pkey.initialize(pkey.getType());
                   okey = pkey.fromJson(okey, key);
                }

                res.put(okey, val);
             }

             return res;
          }
          catch (JsonProcessingException |
                 ReflectiveOperationException e)
          {
             throw new RequestArgumentError("Could not parse " + sval, e);
          }
       }

       /**
        * Create a JSON representation as an array, for the given collection.
        *
        * @param    c
        *           The collection.
        * @param    component
        *           The component's type.  If <code>null</code>, it will be resolved from the value's type.
        *
        * @return   See above.
        */
       protected JsonNode toJsonArray(Collection c, JavaTypeSerializer component)
       {
          if (c == null)
          {
             return JsonNodeFactory.instance.nullNode();
          }

          int size = c.size();

          ArrayNode arr = JsonNodeFactory.instance.arrayNode(size);
          Iterator iter = c.iterator();
          while (iter.hasNext())
          {
             Object val = iter.next();
             JavaTypeSerializer ser = component;
             if (ser == null)
             {
                if (val == null)
                {
                   arr.add(JsonNodeFactory.instance.nullNode());
                   continue;
                }

                ser = getSerializer(val.getClass().getTypeName());
             }

             JsonNode el = ser.toJson(val);
             arr.add(el);
          }

          return arr;
       }

       /**
        * Create a JSON representation of the specified map.
        *
        * @param    map
        *           The map.
        * @param    pkey
        *           The key's serializer.  This is not used, the keys are assumed as string.
        * @param    pval
        *           The value's serializer.  If <code>null</code>, it will be resolved from the value's type.
        *
        * @return   See above.
        */
       protected JsonNode toJsonObject(Map                map,
                                       JavaTypeSerializer pkey,
                                       JavaTypeSerializer pval)
       {
          ObjectNode res = JsonNodeFactory.instance.objectNode();

          Iterator<Map.Entry> iter = map.entrySet().iterator();
          while (iter.hasNext())
          {
             Map.Entry entry = iter.next();
             String key = entry.getKey().toString(); // the keys are always by their String representation

             JavaTypeSerializer ser = pval;
             Object val = entry.getValue();
             if (ser == null)
             {
                if (val == null)
                {
                   res.set(key, JsonNodeFactory.instance.nullNode());
                   continue;
                }

                ser = getSerializer(val.getClass().getTypeName());
             }

             JsonNode node = ser.toJson(entry.getValue());

             res.set(key, node);
          }

          return res;
       }
    }