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