com.senzing.reflect.PropertyReflector Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of senzing-commons Show documentation
Show all versions of senzing-commons Show documentation
Utility classes and functions common to multiple Senzing projects.
The newest version!
package com.senzing.reflect;
import javax.json.*;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Predicate;
import static com.senzing.util.CollectionUtilities.*;
import static java.util.Collections.*;
import static com.senzing.reflect.ReflectionUtilities.*;
/**
* Encapsulates the automatic reflection of bean property values.
*
* @param The type for which the property reflector will obtain the
* properties and operate on.
*/
public class PropertyReflector {
/**
* The {@link Map} of {@link Class} objects to their respective
* {@link PropertyReflector} instances.
*/
private static final Map REFLECTOR_INSTANCES
= new HashMap<>();
/**
* The {@link Map} of {@link String} property names to the corresponding
* accessor {@link Method} values for that property.
*/
private Map accessors;
/**
* The {@link Map} of {@link String} property names to the corresponding
* mutator {@link Method} values for that property.
*/
private Map> mutators;
/**
* Gets the {@link PropertyReflector} instance for the specified {@link
* Class}.
*
* @param cls The {@link Class} for which the {@link PropertyReflector} is
* being requested.
*
* @param The type for the property reflector instance.
* @return The {@link PropertyReflector} for the specified {@link Class}.
*/
public static synchronized PropertyReflector getInstance(Class cls)
{
@SuppressWarnings("unchecked")
PropertyReflector result = REFLECTOR_INSTANCES.get(cls);
if (result == null) {
result = new PropertyReflector<>(cls);
REFLECTOR_INSTANCES.put(cls, result);
}
return result;
}
/**
* Constructs with the specified class and uses reflection (introspection)
* to obtain the accessor and mutator property names and methods.
*
* @param cls The {@link Class} the class to introspect.
*/
protected PropertyReflector(Class cls) {
this.accessors = new LinkedHashMap<>();
this.mutators = new LinkedHashMap<>();
Method[] methods = cls.getMethods();
for (Method method : methods) {
// check if the declaring class is java.lang.Object and skip if so
if (method.getDeclaringClass() == Object.class) continue;
int modifiers = method.getModifiers();
// ignore static methods
if (Modifier.isStatic(modifiers)) continue;
// ignore non-public methods
if (!Modifier.isPublic(modifiers)) continue;
String name = method.getName();
// check if we have a standard getter
if (name.length() > 3 && name.startsWith("get")
&& Character.isUpperCase(name.charAt(3))
&& method.getParameterCount() == 0
&& method.getReturnType() != void.class)
{
String key = name.substring(3, 4).toLowerCase();
if (name.length() > 4) key += name.substring(4);
accessors.put(key, method);
continue;
}
// check for a boolean getter
if (name.length() > 2 && name.startsWith("is")
&& Character.isUpperCase(name.charAt(2))
&& method.getParameterCount() == 0
&& (method.getReturnType() == Boolean.class
|| method.getReturnType() == boolean.class))
{
String key = name.substring(2, 3).toLowerCase();
if (name.length() > 3) key += name.substring(3);
accessors.put(key, method);
continue;
}
// check for a setter
if (name.length() > 3 && name.startsWith("set")
&& Character.isUpperCase(name.charAt(3))
&& method.getParameterCount() == 1
&& method.getReturnType() == void.class)
{
String key = name.substring(3, 4).toLowerCase();
if (name.length() > 4) key += name.substring(4);
List methodList = mutators.get(key);
if (methodList == null) {
methodList = new LinkedList<>();
mutators.put(key, methodList);
}
methodList.add(method);
continue;
}
}
// make the maps unmodifiable
this.accessors = unmodifiableMap(this.accessors);
this.mutators = recursivelyUnmodifiableMap(this.mutators);
}
/**
* Provides the {@link Map} of {@link String} property names to the
* accessor {@link Method} values for that respective property.
*
* @return The {@link Map} of {@link String} property names too the
* accessor {@link Method} values for that respective property.
*/
public Map getAccessors() {
return this.accessors;
}
/**
* Provides the {@link Map} of {@link String} property names to the
* unmodifiable {@link List} of mutator {@link Method} values for that
* respective property.
*
* @return The {@link Map} of {@link String} property names to the
* unmodifiable {@link List} of mutator {@link Method} values
* for that respective property.
*/
public Map> getMutators() {
return this.mutators;
}
/**
* Gets the property value for the specified property key from the specified
* target object.
*
* @param target The target object from which to get the property value.
* @param propertyKey The property key for the property being requested.
*
* @return The property value for the specified property key from the
* specified target object.
*
* @throws IllegalArgumentException If the property key is not recognized.
*
*/
public Object getPropertyValue(T target, String propertyKey)
throws IllegalArgumentException
{
Map accessorMap = this.getAccessors();
Method method = accessorMap.get(propertyKey);
if (method == null) {
// check if this is a known property
if (this.getMutators().containsKey(propertyKey)) {
// NOTE: this is an odd case, but allowed
throw new UnsupportedOperationException(
"The specified property is write-only: " + propertyKey);
} else {
throw new IllegalArgumentException(
"Unrecognized property key: " + propertyKey);
}
}
try {
return method.invoke(target);
} catch (InvocationTargetException|IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* Sets the property value for the specified property key on the specified
* target object.
*
* @param target The target object on which to set the property value.
* @param propertyKey The property key for the property being set.
* @param propertyValue The property value for the property.
*
* @throws IllegalArgumentException If the property key is not recognized.
*/
@SuppressWarnings("unchecked")
public void setPropertyValue(T target, String propertyKey, Object propertyValue)
throws IllegalArgumentException
{
Map> mutatorMap = this.getMutators();
List methods = mutatorMap.get(propertyKey);
boolean invoked = false;
// check if the methods were not found
if (methods == null) {
// check if this is a known property
if (this.getAccessors().containsKey(propertyKey)) {
// NOTE: this is an odd case, but allowed
throw new UnsupportedOperationException(
"The specified property is read-only: " + propertyKey);
} else {
throw new IllegalArgumentException(
"Unrecognized property key: " + propertyKey);
}
}
// if the property value is null then find the best method for setting the
// value to null (no primitives allowed)
if (propertyValue == null) {
invoked = findAndInvokeMutator(
methods, Class::isPrimitive, target, null);
if (invoked) return;
// if we get here then no method was found
throw new NullPointerException(
"The specified value for the property (" + propertyKey + ") cannot "
+ "be null.");
}
// get the value type
Class valueType = propertyValue.getClass();
// for non-null property values, look for an exact type-match first
invoked = findAndInvokeMutator(
methods, (argType -> argType == valueType), target, propertyValue);
if (invoked) return;
// now check for a corresponding primitive mutator if a promoted type
Class primType = getPrimitiveType(valueType);
if (primType != null && primType != valueType) {
invoked = findAndInvokeMutator(
methods, (argType -> argType == primType), target, propertyValue);
if (invoked) return;
}
// check if we have an assignable-from method
invoked = findAndInvokeMutator(
methods,
(argType -> argType.isAssignableFrom(valueType)),
target,
propertyValue);
if (invoked) return;
/*
// COMMENT THIS OUT FOR NOW -- IT DOES NOT NECCESARILY MAKE SENSE TO CONVERT
// FLOAT TO DOUBLE OR VICE-VERSA (OR SHORT TO INT OR LONG)
//
// finally, check if we have an instance of java.lang.Number or the
// primitive equivalent
if (primType != null) {
// get the corresponding promoted type from the primitive type
Class promotedType = getPromotedType(primType);
// check if the promoted type extends java.lang.Number
if (Number.class.isAssignableFrom(promotedType)) {
// get the value as a Number
Number numberValue = (Number) propertyValue;
// iterate over the methods
for (Method method : methods) {
// get the argument type
Class argType = method.getParameterTypes()[0];
// check if the argument type is primitive and if so then promote it
if (argType.isPrimitive()) {
argType = getPromotedType(argType);
}
// test the argument type if a primitive number
if (!Number.class.isAssignableFrom(argType)) continue;
if (getPrimitiveType(argType) == null) continue;
// convert the value to the primitive number type
Object convertedValue = convertPrimitiveNumber(numberValue, argType);
try {
// invoke the method
method.invoke(target, convertedValue);
// return here since invoked
return;
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
*/
// if we get here then no suitable mutator was found
if (propertyValue == null) {
// a null value was specified but all mutators require primitive values
throw new NullPointerException(
"The specified value for the property (" + propertyKey + ") cannot "
+ "be null.");
} else {
throw new ClassCastException(
"The specified value for the property (" + propertyKey + ") was not "
+ "of a valid type: " + propertyValue.getClass().getName());
}
}
/**
* Finds the first {@link Method} satisfying the specified {@link Predicate}
* and invokes it on the specified target {@link Object} with the specified
* property value as a parameter. This method returns true
if
* a method was found and it was invoked, and false
if no method
* was found.
*
* @param methods The {@link List} of {@link Method} instances to search for
* one whose first argument type satisfies the specified
* predicate.
* @param predicate The {@link Predicate} that tests the type of the first
* argument to each {@link Method}.
* @param target The target object on which to invoke the {@link Method}.
* @param propertyValue The property value to pass as a parameter.
*
* @return true
if the {@link Method} was found and invoked, and
* false
if no {@link Method} was found to satisfy the
* {@link Predicate}.
*/
private static boolean findAndInvokeMutator(List methods,
Predicate predicate,
Object target,
Object propertyValue)
{
for (Method method: methods) {
// get the argument type
Class argType = method.getParameterTypes()[0];
// test the argument type against the predicate, skip if it fails
if (!predicate.test(argType)) continue;
try {
// invoke the method
method.invoke(target, propertyValue);
// return true to indicate it was found and invoked
return true;
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
// return false to indicate no method is found
return false;
}
/**
* Uses reflection to extract the properties from the specified {@link Object}
* and recursively construct a {@link JsonObject} from them and return that
* {@link JsonObject}.
*
* @param object The non-null {@link Object} to convert to JSON.
* @return The constructed {@link JsonObject}.
* @throws NullPointerException If the specified parameter is
* null
.
*/
public static JsonObject toJsonObject(Object object)
throws NullPointerException
{
Objects.requireNonNull("The specified object cannot be null");
JsonObjectBuilder job = Json.createObjectBuilder();
return buildJsonObject(job, object).build();
}
/**
* Adds the accessible properties of the specified {@link Object} to the
* specified {@link JsonObjectBuilder} with their respective property names
* as property keys.
*
* @param builder The non-null {@link JsonObjectBuilder} to which to add the
* properties.
* @param object The non-null {@link Object} whose properties should be added
* to the {@link JsonObjectBuilder}.
* @return The specified {@link JsonObjectBuilder}.
* @throws NullPointerException If either of the specified parameters is
* null
.
*/
public static JsonObjectBuilder buildJsonObject(JsonObjectBuilder builder,
Object object)
throws NullPointerException
{
Objects.requireNonNull(
builder, "The specified JsonObjectBuilder cannot be null");
Objects.requireNonNull(
object, "The specified object cannot be null");
IdentityHashMap visitedMap = new IdentityHashMap();
return buildJsonObject(visitedMap, builder, object);
}
/**
* Adds the accessible properties of the specified {@link Object} to the
* specified {@link JsonObjectBuilder} with their respective property names
* as property keys.
*
* @param visited The {@link IdentityHashMap} used to detect circular
* references.
* @param builder The non-null {@link JsonObjectBuilder} to which to add the
* properties.
* @param object The non-null {@link Object} whose properties should be added
* to the {@link JsonObjectBuilder}.
* @return The specified {@link JsonObjectBuilder}.
* @throws NullPointerException If either of the specified parameters is
* null
.
*/
@SuppressWarnings("unchecked")
private static JsonObjectBuilder buildJsonObject(IdentityHashMap visited,
JsonObjectBuilder builder,
Object object)
throws NullPointerException
{
if (visited.containsKey(object)) {
throw new IllegalStateException(
"Circular reference detected for object: " + object);
}
visited.put(object, null);
try {
// get an object map if all keys are strings
Map objectMap = getObjectMap(object);
// initialize the variables as null
PropertyReflector reflector = null;
Map accessors = null;
Set keySet = null;
// check if the object map is null
if (objectMap == null) {
// if null then initialize the property reflector variables and key set
Class> cls = object.getClass();
reflector = PropertyReflector.getInstance(cls);
accessors = reflector.getAccessors();
keySet = accessors.keySet();
} else {
// get the key set from the object map if we have an object map
keySet = objectMap.keySet();
}
// iterate over the properties
for (String propertyKey : keySet) {
// get the property value
Object propertyValue = (objectMap != null)
? objectMap.get(propertyKey)
: reflector.getPropertyValue(object, propertyKey);
// check for a null value
if (propertyValue == null) {
builder.addNull(propertyKey);
continue;
}
Class propertyType = propertyValue.getClass();
if (propertyType.isPrimitive()) {
propertyType = getPromotedType(propertyType);
}
switch (propertyType.getName()) {
case "javax.json.JsonObject":
JsonObject jsonObj = (JsonObject) propertyValue;
builder.add(propertyKey, Json.createObjectBuilder(jsonObj));
break;
case "javax.json.JsonArray":
JsonArray jsonArr = (JsonArray) propertyValue;
builder.add(propertyKey, Json.createArrayBuilder(jsonArr));
break;
case "java.lang.Integer":
case "java.lang.Short":
builder.add(propertyKey, ((Number) propertyValue).intValue());
break;
case "java.lang.Long":
builder.add(propertyKey, ((Number) propertyValue).longValue());
break;
case "java.lang.String":
builder.add(propertyKey, propertyValue.toString());
break;
case "java.lang.Double":
case "java.lang.Float":
builder.add(propertyKey, ((Number) propertyValue).doubleValue());
break;
case "java.lang.Boolean":
builder.add(propertyKey, ((Boolean) propertyValue));
break;
case "java.math.BigDecimal":
builder.add(propertyKey, ((BigDecimal) propertyValue));
break;
case "java.math.BigInteger":
builder.add(propertyKey, ((BigInteger) propertyValue));
break;
default:
if (Collection.class.isAssignableFrom(propertyType)) {
// handle iterating over a collection
JsonArrayBuilder jab = Json.createArrayBuilder();
Collection collection = (Collection) propertyValue;
for (Object elem: collection) {
addToJsonArray(visited, jab, elem);
}
builder.add(propertyKey, jab);
} else if (propertyType.isArray()) {
// handle as an array
JsonArrayBuilder jab = Json.createArrayBuilder();
int length = Array.getLength(propertyValue);
for (int index = 0; index < length; index++) {
Object elem = Array.get(propertyValue, index);
addToJsonArray(visited, jab, elem);
}
builder.add(propertyKey, jab);
} else {
JsonObjectBuilder job = Json.createObjectBuilder();
builder.add(propertyKey,
buildJsonObject(visited, job, propertyValue));
}
}
}
// return the builder
return builder;
} finally {
visited.remove(object);
}
}
/**
* Internal method to add values to a {@link JsonArrayBuilder} when building
* a {@link JsonObject}.
*
* @param visited The {@link IdentityHashMap} used to detect circular
* references.
* @param builder The {@link JsonArrayBuilder} to add the value to.
* @param value The value to be added.
* @return The specified {@link JsonArrayBuilder}.
*/
@SuppressWarnings("unchecked")
private static JsonArrayBuilder addToJsonArray(IdentityHashMap visited,
JsonArrayBuilder builder,
Object value)
{
// check for null
if (value == null) {
builder.addNull();
return builder;
}
if (visited.containsKey(value)) {
throw new IllegalStateException(
"Circular reference detected for object: " + value);
}
visited.put(value, null);
try {
Class valueType = value.getClass();
if (valueType.isPrimitive()) {
valueType = getPromotedType(valueType);
}
switch (valueType.getName()) {
case "javax.json.JsonObject":
JsonObject jsonObj = (JsonObject) value;
builder.add(Json.createObjectBuilder(jsonObj));
break;
case "javax.json.JsonArray":
JsonArray jsonArr = (JsonArray) value;
builder.add(Json.createArrayBuilder(jsonArr));
break;
case "java.lang.Integer":
case "java.lang.Short":
builder.add(((Number) value).intValue());
break;
case "java.lang.Long":
builder.add(((Number) value).longValue());
break;
case "java.lang.String":
builder.add(value.toString());
break;
case "java.lang.Double":
case "java.lang.Float":
builder.add(((Number) value).doubleValue());
break;
case "java.lang.Boolean":
builder.add(((Boolean) value));
break;
case "java.math.BigDecimal":
builder.add(((BigDecimal) value));
break;
case "java.math.BigInteger":
builder.add(((BigInteger) value));
break;
default:
if (Collection.class.isAssignableFrom(valueType)) {
// handle iterating over a collection
JsonArrayBuilder jab = Json.createArrayBuilder();
Collection collection = (Collection) value;
for (Object elem: collection) {
addToJsonArray(visited, jab, elem);
}
builder.add(jab);
} else if (valueType.isArray()) {
// handle as an array
JsonArrayBuilder jab = Json.createArrayBuilder();
int length = Array.getLength(value);
for (int index = 0; index < length; index++) {
Object elem = Array.get(value, index);
addToJsonArray(visited, jab, elem);
}
builder.add(jab);
} else {
JsonObjectBuilder job = Json.createObjectBuilder();
// remove the visited value before recursing
visited.remove(value);
try {
builder.add(buildJsonObject(visited, job, value));
} finally{
visited.put(value, null);
}
}
}
// return the builder
return builder;
} finally {
visited.remove(value);
}
}
/**
*
*/
@SuppressWarnings("unchecked")
private static Map getObjectMap(Object object) {
// check if we have a map
if (!(object instanceof Map)) return null;
Map map = (Map) object;
for (Object key : map.keySet()) {
if (!(key instanceof String)) {
return null;
}
}
return (Map) map;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy