/*
* Copyright (C) 2015 Google Inc.
*
* 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 interactivespaces.util.data.dynamic;
import static java.lang.reflect.Proxy.getInvocationHandler;
import static java.lang.reflect.Proxy.isProxyClass;
import com.google.common.base.Preconditions;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Invocation handler that stores state of the object (JavaBean properties) in the backing map. Calls to hashCode() and
* toString() are delegated to the map.
*
* @author Oleksandr Kelepko
*/
final class BackingMapInvocationHandler implements InvocationHandler {
/**
* Empty array for masking null in {@link BackingMapInvocationHandler#invoke}.
*/
private static final Object[] NO_ARGS = {};
/**
* Prefix of boolean getter methods' names.
*/
private static final String METHOD_PREFIX_IS = "is";
/**
* Prefix of general getter methods' names.
*/
private static final String METHOD_PREFIX_GET = "get";
/**
* Prefix of setter methods' names.
*/
private static final String METHOD_PREFIX_SET = "set";
/**
* Name of the {@link Object#equals(Object)} method.
*/
private static final String OBJECT_EQUALS_METHOD_NAME = "equals";
/**
* Name of the {@link Object#hashCode()} method.
*/
private static final String OBJECT_HASHCODE_METHOD_NAME = "hashCode";
/**
* Name of the {@link Object#toString()} method.
*/
private static final String OBJECT_TOSTRING_METHOD_NAME = "toString";
/**
* The state of the object. Keys are JavaBean property names, values are objects of the following:
* <ul>
* <li>a value of one of the {@link Conversions#SIMPLE_TYPE_CONVERSIONS}
* <li>{@link Map} whose keys are Strings and values are found in this list of types.
* <li>{@link java.util.List} of any of this list of types</li>
* </ul>
*/
private final Map<String, Object> backingMap;
/**
* Type of this dynamic object, is used in equals().
*/
private final String type;
/**
* Constructor.
*
* @param backingMap
* the map that will reflect the state of the object
* @param type
* type this BackingMapInvocationHandler represents (= the interface it implements)
*/
BackingMapInvocationHandler(Map<String, Object> backingMap, Class<?> type) {
this.backingMap = Preconditions.checkNotNull(backingMap);
this.type = type.getName();
}
/**
* Get the state of this object.
*
* @return map holding the values of this object's properties
*/
public Map<String, Object> getBackingMap() {
return backingMap;
}
@Override
public Object invoke(Object target, Method method, Object[] args) throws Throwable {
if (args == null) {
args = NO_ARGS;
}
String methodName = method.getName();
if (method.getDeclaringClass() == Object.class) {
if (args.length == 0) {
if (methodName.equals(OBJECT_HASHCODE_METHOD_NAME)) {
return backingMap.hashCode();
}
if (methodName.equals(OBJECT_TOSTRING_METHOD_NAME)) {
return backingMap.toString();
}
}
if (args.length == 1 && methodName.equals(OBJECT_EQUALS_METHOD_NAME)
&& method.getParameterTypes()[0] == Object.class) {
if (args[0] != null && isProxyClass(args[0].getClass())) {
InvocationHandler handler = getInvocationHandler(args[0]);
return (handler instanceof BackingMapInvocationHandler)
&& type.equals(((BackingMapInvocationHandler) handler).type)
&& backingMap.equals(((BackingMapInvocationHandler) handler).backingMap);
}
return false;
}
// NOTE: clone() is not supported (yet?). Other methods (except finalize()) are final.
}
if (isGetter(method, args)) {
return invokeGetter(method);
}
if (isSetter(method, args)) {
invokeSetter(method, args[0]);
return null;
}
throw new UnsupportedOperationException(String.format("Method is neither a getter nor a setter: %s.", method));
}
/**
* Emulate invoking a getter.
*
* @param method
* method that was called
*
* @return result of calling the getter
*/
private Object invokeGetter(Method method) {
String property = getPropertyName(method.getName());
Object result = backingMap.get(property);
Class<?> returnType = method.getReturnType();
Type genericReturnType = method.getGenericReturnType();
return Conversions.convert(genericReturnType, returnType, result);
}
/**
* Emulate invoking a setter.
*
* @param method
* method that was called
* @param arg
* value to set
*/
private void invokeSetter(Method method, Object arg) {
String property = getPropertyName(method.getName());
if (arg == null) {
// NOTE: backingMap may not support null values,
// so here we remove the entry instead of putting null
backingMap.remove(property);
} else if (isProxyClass(arg.getClass())) {
InvocationHandler handler = getInvocationHandler(arg);
if (handler instanceof BackingMapInvocationHandler) {
BackingMapInvocationHandler dynamicObject = (BackingMapInvocationHandler) handler;
backingMap.put(property, dynamicObject.backingMap);
} else {
backingMap.put(property, arg);
}
} else {
backingMap.put(property, arg);
}
}
/**
* Perform a sanity check that a method is a setter, i.e. has a single parameter.
*
* @param method
* setter method
* @param params
* list of the method's parameters
*
* @return {@code true} if the given method looks like a setter
*
* @throws UnsupportedOperationException
* if the method has a setter-like name, but has number of parameters other than 1
*/
private static boolean isSetter(Method method, Object[] params) {
if (!method.getName().startsWith(METHOD_PREFIX_SET)) {
return false;
}
Class<?> returnType = method.getReturnType();
if (returnType != void.class && returnType != Void.class) {
throw new UnsupportedOperationException(String.format("Setter method must return void: %s.", method));
}
if (params.length != 1) {
throw new UnsupportedOperationException(String.format("Setter method must have exactly 1 parameter: %s", method));
}
return true;
}
/**
* Perform a sanity check that a method is a getter, e.g. has no parameters.
*
* @param method
* getter method
* @param params
* list of the method's parameters
*
* @return {@code true} if the given method seems like a getter
*
* @throws UnsupportedOperationException
* if the method has a getter-like name, but has parameters
*/
private static boolean isGetter(Method method, Object[] params) {
String name = method.getName();
Class<?> returnType = method.getReturnType();
boolean isGetterName =
name.startsWith(METHOD_PREFIX_GET)
|| (name.startsWith(METHOD_PREFIX_IS) && (returnType == Boolean.class || returnType == Boolean.TYPE));
if (!isGetterName) {
return false;
}
if (returnType == void.class || returnType == Void.class) {
throw new UnsupportedOperationException(String.format(
"Getter method must have a return type (not void or Void): %s", method));
}
if (params.length != 0) {
throw new UnsupportedOperationException(String.format("Getter method must be parameterless: %s", method));
}
return true;
}
/**
* Retrieve a JavaBean property name from a method name.
*
* @param methodName
* method name
*
* @return property name
*
* @throws UnsupportedOperationException
* if {@code methodName} does not represent a JavaBean property
*/
private static String getPropertyName(String methodName) {
StringBuilder sb = new StringBuilder(methodName);
// Strip 'is'/'get' or 'set'.
if (methodName.startsWith(METHOD_PREFIX_GET) || methodName.startsWith(METHOD_PREFIX_SET)) {
sb.delete(0, METHOD_PREFIX_GET.length());
} else if (methodName.startsWith(METHOD_PREFIX_IS)) {
sb.delete(0, METHOD_PREFIX_IS.length());
}
if (sb.length() == 0) {
throw new UnsupportedOperationException("Not a getter or setter: " + methodName);
}
sb.setCharAt(0, Character.toLowerCase(sb.charAt(0)));
return sb.toString();
}
}