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