/*
* Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* 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://aws.amazon.com/apache2.0
*
* This file 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.amazonaws.services.dynamodbv2.datamodeling;
import com.amazonaws.annotation.SdkInternalApi;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.Reflect;
import com.amazonaws.services.dynamodbv2.datamodeling.StandardAnnotationMaps.FieldMap;
import com.amazonaws.services.dynamodbv2.datamodeling.StandardAnnotationMaps.TableMap;
import com.amazonaws.util.StringUtils;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Reflection assistant for {@link DynamoDBMapper}
*/
@SdkInternalApi
final class StandardBeanProperties {
/**
* Returns the bean mappings for a given class (caches the results).
*/
@SuppressWarnings("unchecked")
static final <T> Beans<T> of(Class<T> clazz) {
return ((CachedBeans<T>)CachedBeans.CACHE).getBeans(clazz);
}
/**
* Cache of {@link Beans} by class type.
*/
private static final class CachedBeans<T> {
private static final CachedBeans<Object> CACHE = new CachedBeans<Object>();
private final ConcurrentMap<Class<T>,Beans<T>> cache = new ConcurrentHashMap<Class<T>,Beans<T>>();
private final Beans<T> getBeans(Class<T> clazz) {
if (!cache.containsKey(clazz)) {
final TableMap<T> annotations = StandardAnnotationMaps.<T>of(clazz);
final BeanMap<T,Object> map = new BeanMap<T,Object>(clazz, false);
cache.putIfAbsent(clazz, new Beans<T>(annotations, map));
}
return cache.get(clazz);
}
}
/**
* Cache of {@link Bean} mappings by class type.
*/
static final class Beans<T> {
private final DynamoDBMapperTableModel.Properties<T> properties;
private final Map<String,Bean<T,Object>> map;
private Beans(TableMap<T> annotations, Map<String,Bean<T,Object>> map) {
this.properties = new DynamoDBMapperTableModel.Properties.Immutable<T>(annotations);
this.map = Collections.unmodifiableMap(map);
}
final DynamoDBMapperTableModel.Properties<T> properties() {
return this.properties;
}
final Map<String,Bean<T,Object>> map() {
return this.map;
}
}
/**
* Holds the reflection bean properties for a given property.
*/
static final class Bean<T,V> {
private final DynamoDBMapperFieldModel.Properties<V> properties;
private final ConvertibleType<V> type;
private final Reflect<T,V> reflect;
private Bean(FieldMap<V> annotations, Reflect<T,V> reflect, Method getter) {
this.properties = new DynamoDBMapperFieldModel.Properties.Immutable<V>(annotations);
this.type = ConvertibleType.<V>of(getter, annotations);
this.reflect = reflect;
}
final DynamoDBMapperFieldModel.Properties<V> properties() {
return this.properties;
}
final ConvertibleType<V> type() {
return this.type;
}
final Reflect<T,V> reflect() {
return this.reflect;
}
}
/**
* Get/set reflection operations.
*/
static final class MethodReflect<T,V> implements Reflect<T,V> {
private final Method getter, setter;
private MethodReflect(Method getter) {
this.setter = setterOf(getter);
this.getter = getter;
}
@Override
public V get(T object) {
try {
return (V)getter.invoke(object);
} catch (final Exception e) {
throw new DynamoDBMappingException("could not invoke " + getter + " on " + object.getClass(), e);
}
}
@Override
public void set(T object, V value) {
try {
setter.invoke(object, value);
} catch (final Exception e) {
throw new DynamoDBMappingException("could not invoke " + setter + " on " + object.getClass() +
" with value " + value + " of type " + (value == null ? null : value.getClass()), e);
}
}
static Method setterOf(Method getter) {
try {
final String name = "set" + getter.getName().replaceFirst("^(get|is)","");
return getter.getDeclaringClass().getMethod(name, getter.getReturnType());
} catch (final Exception no) {}
return null;
}
}
/**
* Get/set reflection operations with a declaring property.
*/
static final class DeclaringReflect<T,V> implements Reflect<T,V> {
private final Reflect<T,V> reflect;
private final Reflect<T,T> declaring;
private final Class<T> targetType;
private DeclaringReflect(Method getter, Reflect<T,T> declaring, Class<T> targetType) {
this.reflect = new MethodReflect<T,V>(getter);
this.declaring = declaring;
this.targetType = targetType;
}
@Override
public V get(T object) {
final T declaringObject = declaring.get(object);
if (declaringObject == null) {
return null;
}
return reflect.get(declaringObject);
}
@Override
public void set(T object, V value) {
T declaringObject = declaring.get(object);
if (declaringObject == null) {
declaring.set(object, (declaringObject = newInstance(targetType)));
}
reflect.set(declaringObject, value);
}
static <T> T newInstance(Class<T> targetType) {
try {
return targetType.newInstance();
} catch (final Exception e) {
throw new DynamoDBMappingException("could not instantiate " + targetType, e);
}
}
}
/**
* {@link Map} of {@link Bean}
*/
static final class BeanMap<T,V> extends LinkedHashMap<String,Bean<T,V>> {
private final Class<T> clazz;
BeanMap(Class<T> clazz, boolean inherited) {
this.clazz = clazz;
putAll(clazz, inherited);
}
private void putAll(Class<T> clazz, boolean inherited) {
for (final Method method : clazz.getMethods()) {
if (canMap(method, inherited)) {
final FieldMap<V> annotations = StandardAnnotationMaps.<V>of(method, null);
if (!annotations.ignored()) {
final Reflect<T,V> reflect = new MethodReflect<T,V>(method);
putOrFlatten(annotations, reflect, method);
}
}
}
}
private void putOrFlatten(FieldMap<V> annotations, Reflect<T,V> reflect, Method getter) {
if (annotations.flattened()) {
flatten((Class<T>)annotations.targetType(), annotations.attributes(), (Reflect<T,T>)reflect);
} else {
final Bean<T,V> bean = new Bean<T,V>(annotations, reflect, getter);
if (put(bean.properties().attributeName(), bean) != null) {
throw new DynamoDBMappingException("duplicate attribute name");
}
}
}
private void flatten(Class<T> targetType, Map<String,String> attributes, Reflect<T,T> declaring) {
for (final Method method : targetType.getMethods()) {
if (canMap(method, true)) {
String name = fieldNameOf(method);
if ((name = attributes.remove(name)) == null) {
continue;
}
final FieldMap<V> annotations = StandardAnnotationMaps.<V>of(method, name);
if (!annotations.ignored()) {
final Reflect<T,V> reflect = new DeclaringReflect<T,V>(method, declaring, targetType);
putOrFlatten(annotations, reflect, method);
}
}
}
if (!attributes.isEmpty()) { //<- this should be empty by now
throw new DynamoDBMappingException("contains unknown flattened attribute(s): " + attributes);
}
}
private boolean canMap(Method method, boolean inherited) {
if (method.getName().matches("^(get|is).+") == false) {
return false;
} else if (method.getParameterTypes().length != 0) {
return false;
} else if (method.isBridge() || method.isSynthetic()) {
return false;
} else if (method.getDeclaringClass() == Object.class) {
return false;
} else if (!inherited && method.getDeclaringClass() != this.clazz &&
StandardAnnotationMaps.of(method.getDeclaringClass()).attributeType() == null) {
return false;
} else {
return true;
}
}
}
/**
* Gets the field name given the getter method.
*/
static final String fieldNameOf(Method getter) {
final String name = getter.getName().replaceFirst("^(get|is)","");
return StringUtils.lowerCase(name.substring(0, 1)) + name.substring(1);
}
}