com.exadel.aem.toolkit.plugin.metadata.MetadataHandler Maven / Gradle / Ivy
/*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.exadel.aem.toolkit.plugin.metadata;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import com.exadel.aem.toolkit.api.markers._Default;
import com.exadel.aem.toolkit.core.CoreConstants;
import com.exadel.aem.toolkit.plugin.exceptions.ReflectionException;
import com.exadel.aem.toolkit.plugin.maven.PluginRuntime;
import com.exadel.aem.toolkit.plugin.utils.DialogConstants;
/**
* Implements {@link InvocationHandler} to provide a {@link Metadata} instance that exposes the properties of a
* {@code T}-typed source object (usually, an annotation)
* @param Type of the source object
*/
class MetadataHandler implements InvocationHandler {
private static final String METHOD_ANNOTATION_TYPE = "annotationType";
private static final String METHOD_EQUALS = "equals";
private static final String METHOD_FOR_EACH = "forEach";
private static final String METHOD_GET = "getValue";
private static final String METHOD_GET_ANNOTATION = "getAnnotation";
private static final String METHOD_GET_ANY_ANNOTATION = "getAnyAnnotation";
private static final String METHOD_GET_PROPERTY = "getProperty";
private static final String METHOD_HAS_PROPERTY = "hasProperty";
private static final String METHOD_HASH_CODE = "hashCode";
private static final String METHOD_ITERATOR = "iterator";
private static final String METHOD_PUT = "putValue";
private static final String METHOD_SPLITERATOR = "spliterator";
private static final String METHOD_STREAM = "stream";
private static final String METHOD_TO_STRING = "toString";
private static final String METHOD_UNSET = "unsetValue";
private static final String FIELD_SOURCE = "__source";
private static final String FIELD_PROPERTIES = "__properties";
private static final String OPENING_SQUARE = CoreConstants.ARRAY_OPENING;
private static final String CLOSING_SQUARE = CoreConstants.ARRAY_CLOSING;
private static final String TYPE_EXCEPTION_TEMPLATE = "Trying to set a value of type %s to property %s";
private static final String VALUE_EXCEPTION_TEMPLATE = "Invalid value address %s";
private static final int HASH_INITIAL_NUMBER = 17;
private static final int HASH_MULTIPLIER = 37;
private final T source;
private final Class> type;
private final Map properties;
/**
* Constructs an instance of {@code InterfaceHandler} class with its type and the dictionary of property values set
* @param type Type of the source object
* @param properties Dictionary of property values
*/
MetadataHandler(Class type, Map properties) {
this(null, type, properties);
}
/**
* Constructs an instance of {@code InterfaceHandler} class with the source object and the dictionary of property
* values set
* @param source The object used as the source of property values
* @param properties Dictionary of property values used to override and/or supplement those of the source object
*/
MetadataHandler(T source, Map properties) {
this(
source,
source instanceof Annotation ? ((Annotation) source).annotationType() : source.getClass(),
properties);
}
/**
* Constructs an instance of {@code InterfaceHandler} class with the source object, its type, and the dictionary of
* property values set
* @param source The object used as the source of property values
* @param type Type of the source object
* @param properties Dictionary of property values used to override and/or supplement those of the source object
*/
private MetadataHandler(T source, Class> type, Map properties) {
this.source = source;
this.type = type;
this.properties = new HashMap<>();
if (properties != null) {
properties.forEach((key, value) -> putValue(PropertyPath.parse(key), value));
}
}
/* ----------
Invocation
---------- */
/**
* {@inheritDoc}
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
InvocationResult invocation = tryInvokeStandardMember(method, args);
if (invocation.isDone()) {
return invocation.getResult();
}
invocation = tryInvokeMetadataMember(method, args);
if (invocation.isDone()) {
return invocation.getResult();
}
return getProperty(method.getName(), method.getName(), true, true).getValue();
}
/**
* Called from {@link MetadataHandler#invoke(Object, Method, Object[])} to check if the method requested for
* invocation is one of the standard OOTB methods of a Java object, and if so, retrieves the return value of such a
* method
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeStandardMember(Method method, Object[] args) {
if (method.getName().equals(METHOD_ANNOTATION_TYPE)) {
return InvocationResult.done(type);
}
if (method.getName().equals(METHOD_EQUALS) && ArrayUtils.isNotEmpty(args)) {
return InvocationResult.done(equals(args[0]));
}
if (method.getName().equals(METHOD_HASH_CODE)) {
return InvocationResult.done(hashCode());
}
if (method.getName().equals(METHOD_TO_STRING)) {
return InvocationResult.done(toString());
}
return InvocationResult.NOT_DONE;
}
/**
* Called from {@link MetadataHandler#invoke(Object, Method, Object[])} to check if the method requested for
* invocation is one of the methods handled by the current class, and if so, retrieves the return value of such a
* method
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return Either an {@code InvocationResult} instance containing the return value or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeMetadataMember(Method method, Object[] args) {
return Stream.>of(
this::tryInvokeGetAnnotation,
this::tryInvokeGetAnyAnnotation,
this::tryInvokeHasProperty,
this::tryInvokeGetValue,
this::tryInvokeGetProperty,
this::tryInvokePutValue,
this::tryInvokeUnsetValue,
this::tryInvokeIterator,
this::tryInvokeForEach,
this::tryInvokeSpliterator,
this::tryInvokeStream)
.map(func -> func.apply(method, args))
.filter(InvocationResult::isDone)
.findFirst()
.orElse(InvocationResult.NOT_DONE);
}
/**
* Tests if the requested method is {@code getAnnotation()} and retrieves the annotation value
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @param Type of the annotation
* @return Either an {@code InvocationResult} instance containing the return value or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeGetAnnotation(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_GET_ANNOTATION, Class.class)) {
@SuppressWarnings("unchecked")
Annotation result = type.getDeclaredAnnotation((Class) args[0]);
return InvocationResult.done(result);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code getAnyAnnotation()} and retrieves the matching annotation value. Consumes
* an array of annotation types
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @param Type of the annotation
* @return Either an {@code InvocationResult} instance containing the return value or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeGetAnyAnnotation(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_GET_ANY_ANNOTATION, Class[].class)) {
for (Class> cls : (Class>[]) args[0]) {
@SuppressWarnings("unchecked")
Annotation result = type.getDeclaredAnnotation((Class) cls);
if (result != null) {
return InvocationResult.done(result);
}
}
return InvocationResult.done(null);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code hasProperty()} and retrieves whether the given property is present
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return Either an {@code InvocationResult} instance containing the return value or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeHasProperty(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_HAS_PROPERTY, String.class)) {
Property result = getProperty((String) args[0], false);
return InvocationResult.done(result.getValue() != null);
} else if (matchesNameAndArgumentTypes(method, args, METHOD_HAS_PROPERTY, PropertyPath.class)) {
Property result = getProperty((PropertyPath) args[0], false);
return InvocationResult.done(result.getValue() != null);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code getValue()} and retrieves the value of a property of the source object by
* the given name or path
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeGetValue(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_GET, String.class)) {
Property result = getProperty((String) args[0], false);
return InvocationResult.done(result.getValue());
} else if (matchesNameAndArgumentTypes(method, args, METHOD_GET, PropertyPath.class)) {
Property result = getProperty((PropertyPath) args[0], false);
return InvocationResult.done(result.getValue());
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code getProperty()} and retrieves the property of the source object by the
* given name or path
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value or the
* {@link InvocationResult#NOT_DONE} which tells that invocation attempts should continue
*/
private InvocationResult tryInvokeGetProperty(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_GET_PROPERTY, String.class)) {
Property result = getProperty((String) args[0], true);
return InvocationResult.done(result);
} else if (matchesNameAndArgumentTypes(method, args, METHOD_GET_PROPERTY, PropertyPath.class)) {
Property result = getProperty((PropertyPath) args[0], true);
return InvocationResult.done(result);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code putValue()} and assigns a value to the property of the source object by
* the given name or path
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value of the method called or the
* {@link InvocationResult#NOT_DONE} which tells that invocation attempts should continue
*/
private InvocationResult tryInvokePutValue(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_PUT, String.class, Object.class)) {
Object result = putValue((String) args[0], args[1]);
return InvocationResult.done(result);
} else if (matchesNameAndArgumentTypes(method, args, METHOD_PUT, PropertyPath.class, Object.class)) {
Object result = putValue((PropertyPath) args[0], args[1]);
return InvocationResult.done(result);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code unsetValue()} and clears the "overriding" value previously assigned to a
* property of the source object
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value of the method called or the
* {@link InvocationResult#NOT_DONE} which tells that invocation attempts should continue
*/
private InvocationResult tryInvokeUnsetValue(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_UNSET, String.class)) {
Object result = putValue((String) args[0], null);
return InvocationResult.done(result);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code iterator()} and performs appropriate invocation
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value of the method called or the
* {@link InvocationResult#NOT_DONE} which tells that invocation attempts should continue
*/
private InvocationResult tryInvokeIterator(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_ITERATOR)
|| matchesNameAndArgumentTypes(method, args, METHOD_ITERATOR, boolean.class, boolean.class)) {
Iterator result = getIterator(args);
return InvocationResult.done(result);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code forEach()} and performs appropriate invocation
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value of the method called or the
* {@link InvocationResult#NOT_DONE} which tells that invocation attempts should continue
*/
private InvocationResult tryInvokeForEach(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_FOR_EACH, Consumer.class)) {
@SuppressWarnings("unchecked")
Consumer super Property> consumer = (Consumer super Property>) args[0];
new Iterator(false, false).forEachRemaining(consumer);
return InvocationResult.done(null);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code spliterator()} and performs appropriate invocation
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value of the method called or the
* {@link InvocationResult#NOT_DONE} which tells that invocation attempts should continue
*/
private InvocationResult tryInvokeSpliterator(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_SPLITERATOR)
|| matchesNameAndArgumentTypes(method, args, METHOD_SPLITERATOR, boolean.class, boolean.class)) {
Spliterator result = getSpliterator(args);
return InvocationResult.done(result);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests if the requested method is {@code stream()} and performs appropriate invocation
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}
* @return {@code InvocationResult} instance containing either the return value of the method called or the
* {@link InvocationResult#NOT_DONE} which effectively tells that invocation attempts should continue
*/
private InvocationResult tryInvokeStream(Method method, Object[] args) {
if (matchesNameAndArgumentTypes(method, args, METHOD_STREAM)
|| matchesNameAndArgumentTypes(method, args, METHOD_STREAM, boolean.class, boolean.class)) {
Spliterator spliterator = getSpliterator(args);
Stream stream = StreamSupport.stream(spliterator, false);
return InvocationResult.done(stream);
}
return InvocationResult.NOT_DONE;
}
/**
* Tests whether the provided method and arguments correspond to the given method and argument types
* @param method The method to check
* @param args An array of objects containing the values of the arguments passed in the method invocation
* @param name The name of the method to match
* @param argTypes The types of arguments to match
* @return True or false
*/
private static boolean matchesNameAndArgumentTypes(Method method, Object[] args, String name, Class>... argTypes) {
if (!StringUtils.equals(method.getName(), name)) {
return false;
}
if (ArrayUtils.isEmpty(argTypes)) {
return true;
}
int argsLength = ArrayUtils.getLength(args);
if (argsLength < argTypes.length) {
return false;
}
for (int i = 0; i < argTypes.length; i++) {
if (args[i] == null || !ClassUtils.isAssignable(args[i].getClass(), argTypes[i])) {
return false;
}
}
return true;
}
/* ---------------
logic
--------------- */
/**
* Retrieves an {@link Iterator} instance that can be used to iterate through the properties of the source object
* @param args An array of objects containing the values of the arguments passed to
* {@link MetadataHandler#invoke(Object, Method, Object[])}. We expect the 0th argument to be the flag
* determining whether array values should be iterated as separate entities, or else the array is
* yielded as is
* @return {@code Iterator} instance
*/
private Iterator getIterator(Object[] args) {
boolean deepRead = false;
boolean expandArrays = false;
if (ArrayUtils.isNotEmpty(args)
&& args[0] != null
&& ClassUtils.isAssignable(args[0].getClass(), boolean.class)) {
deepRead = (boolean) args[0];
}
if (ArrayUtils.getLength(args) > 1
&& args[1] != null
&& ClassUtils.isAssignable(args[0].getClass(), boolean.class)) {
expandArrays = (boolean) args[1];
}
return new Iterator(deepRead, expandArrays);
}
/**
* Retrieves an {@link Spliterator} instance that can be used to iterate through the properties of the source
* object
* @param args An array of objects containing per the contract of the
* {@link MetadataHandler#invoke(Object, Method, Object[])} method
* @return {@code Spliterator} instance
*/
private Spliterator getSpliterator(Object[] args) {
Iterator iterator = getIterator(args);
return Spliterators.spliteratorUnknownSize(iterator, 0);
}
/* -----------------------
#get logic
----------------------- */
/**
* Retrieves a {@link Property} object by the given path
* @param path The path within the current object to construct the property from
* @param throwOnMissing {@code True} to throw an exception if the property is not found
* @return A nullable {@code Property} object
*/
private Property getProperty(String path, boolean throwOnMissing) {
return getProperty(PropertyPath.parse(path), throwOnMissing);
}
/**
* Retrieves a {@link Property} object by the given path
* @param path The {@link PropertyPath} instance that manifests the path within the current object to
* construct the property from
* @param throwOnMissing {@code True} to throw an exception if the property is not found
* @return A nullable {@code Property} object
* @see PropertyPath
*/
private Property getProperty(PropertyPath path, boolean throwOnMissing) {
PropertyPathElement element = path.getElements().remove();
String name = element.getName();
if (FIELD_SOURCE.equals(name)) {
return new Property(name, source);
} else if (FIELD_PROPERTIES.equals(name)) {
return new Property(name, properties);
}
Property result = getProperty(path.getPath(), name, throwOnMissing, true);
if (result.getValue() == null) {
return result;
}
if (result.getType().isArray() && element.hasIndex()) {
if (element.getIndex() < Array.getLength(result.getValue())) {
result.setValue(Array.get(result.getValue(), element.getIndex()));
} else {
result.setValue(null);
return result;
}
}
if (result.getComponentType().isAnnotation() && !path.getElements().isEmpty()) {
return Metadata.from((Annotation) result.getValue()).getProperty(path);
}
return result;
}
/**
* Retrieves a {@link Property} object by the given path
* @param path The "complete" path within the current object to construct the property from
* @param name A string representing the current path chunk
* @param throwOnMissingMethod {@code True} to throw an exception if the property is not found
* @param substituteMissingValue {@code True} to substitute a {@code }
* @return A nullable {@code Property} object
* @see PropertyPath
*/
private Property getProperty(String path, String name, boolean throwOnMissingMethod, boolean substituteMissingValue) {
try {
Method method = type.getDeclaredMethod(name);
Object value = getDefaultReturnValue(method, substituteMissingValue);
if (source != null) {
value = method.invoke(source);
}
if (properties != null && properties.containsKey(name)) {
value = properties.get(method.getName());
value = value != null ? value : getDefaultReturnValue(method, substituteMissingValue);
}
return new MethodBackedProperty(path, method, value);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
if (throwOnMissingMethod) {
PluginRuntime
.context()
.getExceptionHandler()
.handle(new ReflectionException(String.format(VALUE_EXCEPTION_TEMPLATE, name), e));
}
}
return Property.EMPTY;
}
/**
* Retrieves a default value for a method based on the method return type. This method mainly address the case when
* a {@link Metadata} is created from just type and aims to make sure that as many annotation properties as possible
* do not return {@code null}. Note: currently this method is not comprehensive since it does not cover all the
* possible annotation properties' types
* @param method {@code Method} instance
* @param createMissingObjects {@code True} to generate missing objects, such as nested arrays of annotation
* instances. Useful when querying annotation data but must be turned off when data is
* being stored. In the latter case, the stored data will end up in a "detached" ad-hoc
* object
* @return A nullable value
*/
@SuppressWarnings("unchecked")
private static Object getDefaultReturnValue(Method method, boolean createMissingObjects) {
if (method.getDefaultValue() != null) {
return method.getDefaultValue();
}
if (method.getReturnType().isArray() && createMissingObjects) {
return Array.newInstance(method.getReturnType().getComponentType(), 0);
}
if (method.getReturnType().isAnnotation() && createMissingObjects) {
return Metadata.from((Class extends Annotation>) method.getReturnType());
}
if (method.getReturnType().equals(String.class)) {
return StringUtils.EMPTY;
}
if (method.getReturnType().equals(Class.class)) {
return _Default.class;
}
if (ClassUtils.primitiveToWrapper(method.getReturnType()).equals(Boolean.class)) {
return false;
}
if (ClassUtils.primitiveToWrapper(method.getReturnType()).equals(Integer.class)) {
return 0;
}
if (ClassUtils.primitiveToWrapper(method.getReturnType()).equals(Long.class)) {
return 0L;
}
if (ClassUtils.primitiveToWrapper(method.getReturnType()).equals(Float.class)) {
return 0f;
}
if (ClassUtils.primitiveToWrapper(method.getReturnType()).equals(Double.class)) {
return 0d;
}
return null;
}
/* -----------------------
#put logic
----------------------- */
/**
* Assigns a value to the property of the source object by the given path
* @param path The path that manifests a property of the current object
* @param value The value to assign
* @return The value assigned
*/
private Object putValue(String path, Object value) {
return putValue(PropertyPath.parse(path), value);
}
/**
* Assigns a value to the property of the source object by the given path
* @param path {@link PropertyPath} instance that manifests a property of the current object
* @param value The value to assign
* @return The value assigned
*/
private Object putValue(PropertyPath path, Object value) {
if (path.getElements().size() > 1) {
return putInTree(path, value);
}
PropertyPathElement element = path.getElements().remove();
Property currentProperty = getProperty(path.getPath(), element.getName(), true, false);
if (Property.EMPTY.equals(currentProperty)) {
// Probably a nonexistent property name. An exception is already handled
return null;
}
boolean mustWriteToArray = currentProperty.getType().isArray() && element.hasIndex();
if (mustWriteToArray) {
if (!validateValueType(element, path, value, true)
|| !validateArrayBounds(element, path, currentProperty.getValue())) {
return null;
}
Object modifiedValue = appendToArrayIfNeeded(
currentProperty.getValue(),
currentProperty.getComponentType(),
element.getIndex());
properties.put(element.getName(), modifiedValue);
Array.set(modifiedValue, element.getIndex(), value);
return value;
}
if (validateValueType(element, path, value)) {
return properties.put(element.getName(), value);
}
return null;
}
/**
* Called by {@link MetadataHandler#putValue(PropertyPath, Object)} to assign a value to a property of the source
* manifested by a compound (tree-like) path
* @param path {@link PropertyPath} instance that manifests a property of the current object
* @param value The value to assign
* @return The value assigned
*/
@SuppressWarnings("unchecked")
private Object putInTree(PropertyPath path, Object value) {
PropertyPathElement element = path.getElements().remove();
Property currentProperty = getProperty(path.getPath(), element.getName(), true, false);
if (Property.EMPTY.equals(currentProperty)) {
// Probably a nonexistent property name. An exception is already handled
return null;
}
Object existingValue = currentProperty.getValue();
boolean mustWriteToArray = currentProperty.getType().isArray() && element.hasIndex();
if (mustWriteToArray) {
if (!validateValueType(element, path, currentProperty.getValue(), true)
|| !validateArrayBounds(element, path, currentProperty.getValue())) {
return null;
}
existingValue = appendToArrayIfNeeded(
currentProperty.getValue(),
currentProperty.getComponentType(),
element.getIndex());
properties.put(element.getName(), existingValue);
existingValue = Array.get(existingValue, element.getIndex());
}
if (!currentProperty.getComponentType().isAnnotation()) {
PluginRuntime
.context()
.getExceptionHandler()
.handle(new ReflectionException(String.format(VALUE_EXCEPTION_TEMPLATE, path)));
return null;
}
Metadata metadata;
if (!(existingValue instanceof Metadata)) {
metadata = existingValue != null
? Metadata.from((Annotation) existingValue)
: (Metadata) Metadata.from((Class extends Annotation>) currentProperty.getComponentType());
if (mustWriteToArray) {
Array.set(properties.get(element.getName()), element.getIndex(), metadata);
} else {
properties.put(element.getName(), metadata);
}
} else {
metadata = (Metadata) existingValue;
}
return metadata.putValue(path, value);
}
/**
* Called by a property-assigning routine to convert the provided source object into an array or else extend the
* provided array and append to it a new {@link Metadata} object (probably a proxied annotation) built upon the
* given {@code contentType}. This method can be used to construct a new metadata instance and fill in its
* array-typed properties via notation like {@code /my/property[0] = "value", /my/property[1} = "value2, etc
* @param source The array-typed object to modify
* @param componentType The type of an element of the array
* @param index The index of the element to modify
* @return The modified array or the original object if not of an array type or else the index is invalid
*/
@SuppressWarnings("unchecked")
private static Object appendToArrayIfNeeded(Object source, Class> componentType, int index) {
int sourceLength = (source == null || !source.getClass().isArray()) ? 0 : Array.getLength(source);
if (index < sourceLength) {
return source;
}
Object newArray = Array.newInstance(componentType, sourceLength + 1);
for (int i = 0; i < sourceLength; i++) {
Array.set(newArray, i, Array.get(source, i));
}
if (componentType.isAnnotation()) {
Array.set(newArray, sourceLength, Metadata.from((Class extends Annotation>) componentType));
}
return newArray;
}
/* ----------------
Validation logic
---------------- */
/**
* Called by a property-assigning routine to validate that the value to assign to a property is of the same type as
* the property itself
* @param element The {@link PropertyPathElement} instance that manifests the terminal (last-in-the-path) member
* within the source object to assign the value to
* @param path The {@link PropertyPath} instance that manifests the path within the source object
* @param value The value to assign
* @return True or false
*/
private boolean validateValueType(PropertyPathElement element, PropertyPath path, Object value) {
return validateValueType(element, path, value, false);
}
/**
* Called by a property-assigning routine to validate that the value to assign to a property is of the same type as
* the property itself
* @param element The {@link PropertyPathElement} instance that manifests the terminal (last-in-the-path) member
* within the source object to assign the value to
* @param path The {@link PropertyPath} instance that manifests the path within the source object
* @param value The value to assign
* @param lookUpArray {@code True} to test the component type of the array-typed property
* @return True or false
*/
private boolean validateValueType(PropertyPathElement element, PropertyPath path, Object value, boolean lookUpArray) {
if (value == null) {
return true;
}
boolean result;
try {
Method method = type.getDeclaredMethod(element.getName());
Class> methodType = method.getReturnType();
if (methodType.isArray() && !value.getClass().isArray() && lookUpArray) {
methodType = methodType.getComponentType();
}
result = ClassUtils.isAssignable(value.getClass(), methodType);
} catch (NoSuchMethodException e) {
// An exception is not expected here because the method name has already been trialed
return false;
}
if (!result) {
PluginRuntime
.context()
.getExceptionHandler()
.handle(new ReflectionException(String.format(
TYPE_EXCEPTION_TEMPLATE,
value.getClass().getSimpleName(),
path)));
}
return result;
}
/**
* Called by a property-assigning routine to validate that the index of the array-typed property is within the
* bounds of the array
* @param element A {@link PropertyPathElement} instance that signifies the terminal (last-in-the-path) member
* within the source object to assign the value to. It is expected to bear a valid index which is
* used for validation of {@code target}
* @param path The {@link PropertyPath} instance that manifests the path within the source object
* @param target The array-typed or else convertible to array value which is checked with the index
* @return True if the index falls within the array bounds or else false
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean validateArrayBounds(PropertyPathElement element, PropertyPath path, Object target) {
int length = (target == null || !target.getClass().isArray()) ? 0 : Array.getLength(target);
if (element.getIndex() > length) {
PluginRuntime
.context()
.getExceptionHandler()
.handle(new ReflectionException(String.format(VALUE_EXCEPTION_TEMPLATE, path)));
return false;
}
return true;
}
/* -----------------
Silent invocation
----------------- */
/**
* Invokes the given method on the source object and returns the result or else {@code null} if the invocation
* failed with an exception
* @param method The method to invoke
* @param source The source object to invoke the method on
* @return A nullable value
*/
private static Object invokeSilently(Method method, Object source) {
try {
return method.invoke(source);
} catch (IllegalAccessException | InvocationTargetException e) {
return null;
}
}
/* -------------
Serialization
------------- */
/**
* {@inheritDoc}
*/
@Override
public String toString() {
if (source == null && properties.isEmpty()) {
return String.valueOf(type);
}
StringBuilder result = new StringBuilder(CoreConstants.SEPARATOR_AT)
.append(type.getName())
.append(DialogConstants.OPENING_CURLY);
for (Method method : type.getDeclaredMethods()) {
Object methodValue = source != null ? invokeSilently(method, source) : null;
methodValue = properties.getOrDefault(method.getName(), methodValue);
boolean isDefaultMethodValue = false;
if (methodValue == null) {
methodValue = getDefaultReturnValue(method, true);
isDefaultMethodValue = true;
}
result.append(method.getName())
.append(CoreConstants.EQUALITY_SIGN)
.append(isDefaultMethodValue ? "(default) " : StringUtils.EMPTY);
if (methodValue != null && methodValue.getClass().isArray()) {
result.append(toArrayString(methodValue));
} else if (methodValue != null && methodValue.getClass().isAnnotation()) {
result.append(Metadata.from((Annotation) methodValue));
} else {
result.append(methodValue);
}
result.append(DialogConstants.SEPARATOR_SEMICOLON);
}
return StringUtils.stripEnd(result.toString(), DialogConstants.SEPARATOR_SEMICOLON) + DialogConstants.CLOSING_CURLY;
}
/**
* Called by {@link MetadataHandler#toString()} to convert the given array to a string representation
* @param array The array to convert
* @return A string representation of the array
*/
private static String toArrayString(Object array) {
StringBuilder result = new StringBuilder(OPENING_SQUARE);
for (int i = 0; i < Array.getLength(array); i++) {
Object entry = Array.get(array, i);
if (entry instanceof Annotation) {
result.append(Metadata.from((Annotation) entry));
} else {
result.append(entry);
}
result.append(CoreConstants.SEPARATOR_COMMA);
}
return StringUtils.strip(result.toString(), CoreConstants.SEPARATOR_COMMA) + CLOSING_SQUARE;
}
/* -------------------------
Standard method overrides
------------------------- */
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Metadata)) {
return false;
}
Metadata that = (Metadata) o;
return new EqualsBuilder()
.append(source, that.getValue(FIELD_SOURCE))
.append(type, that.annotationType())
.append(properties, that.getValue(FIELD_PROPERTIES))
.isEquals();
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return new HashCodeBuilder(HASH_INITIAL_NUMBER, HASH_MULTIPLIER)
.append(source)
.append(type)
.append(properties)
.toHashCode();
}
/* ---------------
Utility classes
--------------- */
/**
* Implements {@link java.util.Iterator} to provide sequential access to properties of an object (usually a proxied
* annotation), including (optionally) nested properties and array elements
*/
private class Iterator implements java.util.Iterator {
private final boolean deepRead;
private final boolean expandArrays;
private final Queue properties;
/**
* Constructs a new {@code Iterator} instance
* @param deepRead {@code True} to read nested properties
* @param expandArrays {@code True} to expand array elements into separate elements of iteration
*/
Iterator(boolean deepRead, boolean expandArrays) {
this.deepRead = deepRead;
this.expandArrays = expandArrays;
this.properties = new LinkedList<>();
collect(null, StringUtils.EMPTY, this.properties);
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
return !properties.isEmpty();
}
/**
* {@inheritDoc}
*/
@Override
public Property next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return properties.remove();
}
/**
* Analyzes the given annotation and adds its properties to the given queue
* @param target The annotation to analyze
* @param pathPrefix The part of the path to prepend to the property names. Usually signifies nested properties
* @param collection The queue to add the properties to. It will be afterwards iterated with
* {@link Iterator#next()}
*/
private void collect(Annotation target, String pathPrefix, Queue collection) {
Method[] methods = target != null
? target.annotationType().getDeclaredMethods()
: type.getDeclaredMethods();
for (Method method : methods) {
collect(target, method, pathPrefix, collection);
}
}
/**
* Adds a property manifested by the given {@code Annotation} and {@code Method} the given queue
* @param target The annotation being analyzed
* @param method The method to retrieve a value from
* @param pathPrefix The part of the path to prepend to the property names. Usually signifies nested properties
* @param collection The queue to add the properties to. It will be afterwards iterated with
* {@link Iterator#next()}
*/
private void collect(Annotation target, Method method, String pathPrefix, Queue collection) {
String path = joinPathChunks(pathPrefix, method.getName());
Class> propertyType = method.getReturnType();
Object value = target != null
? invokeSilently(method, target)
: invokeInCurrentObjectSilently(method);
if (value == null) {
value = getDefaultReturnValue(method, true);
}
if (deepRead && propertyType.isAnnotation()) {
collect((Annotation) value, path, collection);
} else if (expandArrays && propertyType.isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
String indexedPath = path + OPENING_SQUARE + i + CLOSING_SQUARE;
if (deepRead && propertyType.getComponentType().isAnnotation()) {
collect((Annotation) Array.get(value, i), indexedPath, collection);
} else {
collection.add(new Property(indexedPath, Array.get(value, i)));
}
}
} else {
collection.add(new MethodBackedProperty(path, method, value));
}
}
/**
* Invokes the given method on the source object and returns the result or else {@code null} if the invocation
* fails
* @param method The method to invoke
* @return A nullable value
*/
private Object invokeInCurrentObjectSilently(Method method) {
if (MetadataHandler.this.properties != null && MetadataHandler.this.properties.containsKey(method.getName())) {
return MetadataHandler.this.properties.get(method.getName());
}
return source != null ? invokeSilently(method, source) : null;
}
/**
* Joins two property path chunks with a slash
* @param left The left part of the path
* @param right The right part of the path
* @return A string representing the joined path
*/
private String joinPathChunks(String left, String right) {
return left
+ (StringUtils.isNoneEmpty(left, right) ? CoreConstants.SEPARATOR_SLASH : StringUtils.EMPTY)
+ right;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy