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;
   }
}