# PojoSerializer.java

    /*
    ** Module   : PojoSerializer.java
    ** Abstract : A serializer and parser for POJO (Plain Old Java Object) instances.
    **
    ** Copyright (c) 2022, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20220323 Created initial version.
    **     TJD 20220504 Java 11 compatibility minor changes
    ** 002 SBI 20230627 Changed toJson to serialize POJOs that are proxied by PropertiesDescriptor.
    */
    /*
    ** 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.util.*;
    import java.util.concurrent.*;

    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.node.*;
    import com.goldencode.p2j.persist.orm.PropertiesDescriptor;
    import com.goldencode.p2j.rest.*;

    /**
     * A parser and serializer for POJO (Plain Old Java Object) instances.
     */
    public class PojoSerializer
    extends JavaTypeSerializer<PojoType>
    {
       /** A cache of getter/setter methods for each {@link PojoType}, by their Java field names. */
       private static final Map<Class<? extends PojoType>, Map<String, Method[]>>
       PROPERTY_CACHE = new ConcurrentHashMap<>();

       /**
        * Create a new serializer for {@link PojoType} type.
        */
       public PojoSerializer()
       {
          super(PojoType.class);
       }

       /**
        * Create an initial (default) instance for this serializer.
        *
        * @param    definitionType
        *           The type as it appears at the parameter's definition.
        *
        * @return   A new instance of the parameter's type.
        */
       @Override
       public PojoType initialize(Class<? extends PojoType> definitionType)
       throws ReflectiveOperationException
       {
          return definitionType.getDeclaredConstructor().newInstance();
       }

       /**
        * 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.
        */
       @Override
       public PojoType fromJson(PojoType arg, String sval)
       throws RequestArgumentError
       {
          Map<String, Object> pojo = readMap(STRING_SERIALIZER, null, sval, HashMap.class);

          Map<String, Method[]> properties = getProperties(arg.getClass());
          for (Map.Entry<String, Object> entry : pojo.entrySet())
          {
             String field = entry.getKey();
             Object val = entry.getValue();

             Method[] pd = properties.get(field);
             try
             {
                pd[1].invoke(arg, val);
             }
             catch (IllegalAccessException   |
                    IllegalArgumentException |
                    InvocationTargetException e)
             {
                throw new RequestArgumentError("Could not write field " + field + " from pojo " + arg.getClass(),
                                               e);
             }
          }

          return arg;
       }

       /**
        * Serialize the given instance to JSON.
        *
        * @param    val
        *           The instance to serialize.
        *
        * @return   The JSON representation of this instance.
        */
       @Override
       public JsonNode toJson(PojoType val)
       {
          ObjectNode res = JsonNodeFactory.instance.objectNode();
          if (val instanceof PropertiesDescriptor) // proxied pojo
          {
             Map<String, Object> props = ((PropertiesDescriptor) val).getPropertyValues();
             for (Map.Entry<String, Object> entry : props.entrySet())
             {
                String prop = entry.getKey();
                Object value = entry.getValue();

                JsonNode node;
                if (value == null)
                {
                   node = JsonNodeFactory.instance.nullNode();
                }
                else
                {
                   JavaTypeSerializer fieldSerializer = getSerializer(value.getClass().getTypeName(), -1);
                   node = fieldSerializer.toJson(value);
                }

                res.set(prop, node);
             }
          }
          else
          {
             Map<String, Method[]> properties = getProperties(val.getClass());
             for (Map.Entry<String, Method[]> entry : properties.entrySet())
             {
                String field = entry.getKey();
                Method[] pd = entry.getValue();

                Object value;
                try
                {
                   value = pd[0].invoke(val);
                }
                catch (IllegalAccessException |
                       IllegalArgumentException |
                       InvocationTargetException e)
                {
                   // TODO: other error???
                   throw new RuntimeException("Could not read field " + field + " from pojo " + val.getClass(),
                                              e);
                }

                JsonNode node;
                if (value == null)
                {
                   node = JsonNodeFactory.instance.nullNode();
                }
                else
                {
                   JavaTypeSerializer fieldSerializer = getSerializer(value.getClass().getTypeName());
                   node = fieldSerializer.toJson(value);
                }

                res.set(field, node);
             }
          }
          return res;
       }

       /**
        * Resolve the POJO serializable properties for the given type.  These properties include only
        * non-transient fields defined directly defined in the specified type, for which a getter and a setter
        * method exists, following the Java bean convention.
        *
        * @param    clazz
        *           The POJO type.
        *
        * @return   The mapping of Java fields to their getter (index 0) and setter (index 1) methods.
        */
       private Map<String, Method[]> getProperties(Class<? extends PojoType> clazz)
       {
          Map<String, Method[]> res = PROPERTY_CACHE.get(clazz);
          if (res != null)
          {
             return res;
          }

          res = new HashMap<>();
          Field[] fields = clazz.getDeclaredFields();
          for (int i = 0; i < fields.length; i++)
          {
             Field f = fields[i];

             if (Modifier.isTransient(f.getModifiers()))
             {
                continue;
             }

             // look for getter and setter methods
             String suffix = f.getName();
             suffix = Character.toUpperCase(suffix.charAt(0)) + suffix.substring(1);

             String setterName = "set" + suffix;
             String getterName = ((f.getType() == Boolean.class || f.getType() == boolean.class) ? "is" : "get") +
                                 suffix;

             try
             {
                Method getter = clazz.getMethod(getterName);
                Method setter = clazz.getMethod(setterName, f.getType());

                res.put(f.getName(), new Method[] { getter, setter });
             }
             catch (NoSuchMethodException |
                    SecurityException e)
             {
                continue;
             }
          }

          PROPERTY_CACHE.put(clazz, res);
          return res;
       }
    }