
com.aerospike.mapper.tools.ClassCacheEntry Maven / Gradle / Ivy
package com.aerospike.mapper.tools;
import com.aerospike.client.AerospikeException;
import com.aerospike.client.Bin;
import com.aerospike.client.Record;
import com.aerospike.client.cdt.MapOrder;
import com.aerospike.client.policy.*;
import com.aerospike.mapper.annotations.*;
import com.aerospike.mapper.tools.TypeUtils.AnnotatedType;
import com.aerospike.mapper.tools.configuration.BinConfig;
import com.aerospike.mapper.tools.configuration.ClassConfig;
import com.aerospike.mapper.tools.configuration.KeyConfig;
import org.apache.commons.lang3.StringUtils;
import javax.validation.constraints.NotNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
public class ClassCacheEntry {
public static final String VERSION_PREFIX = "@V";
public static final String TYPE_PREFIX = "@T:";
public static final String TYPE_NAME = ".type";
private String namespace;
private String setName;
private Integer ttl = null;
private boolean mapAll = true;
private Boolean sendKey = null;
private Boolean durableDelete = null;
private int version = 1;
private final Class clazz;
private ValueType key;
private String keyName = null;
private final TreeMap values = new TreeMap<>();
private final ClassCacheEntry> superClazz;
private final int binCount;
private final AeroMapper mapper;
private Map ordinals = null;
private Set fieldsWithOrdinals = null;
private final ClassConfig classConfig;
private final Policy readPolicy;
private final WritePolicy writePolicy;
private final BatchPolicy batchPolicy;
private final QueryPolicy queryPolicy;
private final ScanPolicy scanPolicy;
private String[] constructorParamBins;
private Object[] constructorParamDefaults;
private Constructor constructor;
/**
* When there are subclasses, we need to store the type information to be able to re-create an instance of the same type. As the
* class name can be verbose, we provide the ability to set a string representing the class name. This string must be unique for all classes.
*/
private String shortenedClassName;
private boolean isChildClass = false;
// package visibility only.
ClassCacheEntry(@NotNull Class clazz, AeroMapper mapper, ClassConfig config,
@NotNull Policy readPolicy, @NotNull WritePolicy writePolicy,
@NotNull BatchPolicy batchPolicy, @NotNull QueryPolicy queryPolicy,
@NotNull ScanPolicy scanPolicy) {
this.clazz = clazz;
this.mapper = mapper;
this.classConfig = config;
this.readPolicy = readPolicy;
this.writePolicy = writePolicy;
this.batchPolicy = batchPolicy;
this.scanPolicy = scanPolicy;
this.queryPolicy = queryPolicy;
AerospikeRecord recordDescription = clazz.getAnnotation(AerospikeRecord.class);
if (recordDescription == null && config == null) {
throw new AerospikeException("Class " + clazz.getName() + " is not augmented by the @AerospikeRecord annotation");
}
else if (recordDescription != null) {
this.namespace = ParserUtils.getInstance().get(recordDescription.namespace());
this.setName = ParserUtils.getInstance().get(recordDescription.set());
this.ttl = recordDescription.ttl();
this.mapAll = recordDescription.mapAll();
this.version = recordDescription.version();
this.sendKey = recordDescription.sendKey();
this.durableDelete = recordDescription.durableDelete();
this.shortenedClassName = recordDescription.shortName();
}
if (config != null) {
config.validate();
this.overrideSettings(config);
}
this.loadFieldsFromClass(clazz, this.mapAll, config);
this.loadPropertiesFromClass(clazz, config);
this.superClazz = ClassCache.getInstance().loadClass(this.clazz.getSuperclass(), this.mapper);
this.binCount = this.values.size() + (superClazz != null ? superClazz.binCount : 0);
if (this.binCount == 0) {
throw new AerospikeException("Class " + clazz.getSimpleName() + " has no values defined to be stored in the database");
}
this.formOrdinalsFromValues();
this.findConstructor();
if (StringUtils.isBlank(this.shortenedClassName)) {
this.shortenedClassName = clazz.getSimpleName();
}
ClassCache.getInstance().setStoredName(this, this.shortenedClassName);
this.checkRecordSettingsAgainstSuperClasses();
}
public Policy getReadPolicy() {
return readPolicy;
}
public WritePolicy getWritePolicy() {
return writePolicy;
}
public BatchPolicy getBatchPolicy() {
return batchPolicy;
}
public QueryPolicy getQueryPolicy() {
return queryPolicy;
}
public ScanPolicy getScanPolicy() {
return scanPolicy;
}
public Class> getUnderlyingClass() {
return this.clazz;
}
public ClassConfig getClassConfig() {
return this.classConfig;
}
public String getShortenedClassName() {
return this.shortenedClassName;
}
private void overrideSettings(ClassConfig config) {
if (!StringUtils.isBlank(config.getNamespace())) {
this.namespace = config.getNamespace();
}
if (!StringUtils.isBlank(config.getSet())) {
this.setName = config.getSet();
}
if (config.getTtl() != null) {
this.ttl = config.getTtl();
}
if (config.getVersion() != null) {
this.version = config.getVersion();
}
if (config.getDurableDelete() != null) {
this.durableDelete = config.getDurableDelete();
}
if (config.getMapAll() != null) {
this.mapAll = config.getMapAll();
}
if (config.getSendKey() != null) {
this.sendKey = config.getSendKey();
}
if (config.getShortName() != null) {
this.shortenedClassName = config.getShortName();
}
}
public boolean isChildClass() {
return isChildClass;
}
private void checkRecordSettingsAgainstSuperClasses() {
if (!StringUtils.isBlank(this.namespace) && !StringUtils.isBlank(this.setName)) {
// This class defines it's own namespace + set, it is only a child class if it's closest named superclass is the same as ours.
this.isChildClass = false;
ClassCacheEntry> thisEntry = this.superClazz;
while (thisEntry != null) {
if ((!StringUtils.isBlank(thisEntry.getNamespace())) && (!StringUtils.isBlank(thisEntry.getSetName()))) {
if (this.namespace.equals(thisEntry.getNamespace()) && this.setName.equals(thisEntry.getSetName())) {
this.isChildClass = true;
}
break;
}
thisEntry = thisEntry.superClazz;
}
}
else {
// Otherwise this is a child class, find the set and namespace from the closest highest class
this.isChildClass = true;
ClassCacheEntry> thisEntry = this.superClazz;
while (thisEntry != null) {
if ((!StringUtils.isBlank(thisEntry.getNamespace())) && (!StringUtils.isBlank(thisEntry.getSetName()))) {
this.namespace = thisEntry.getNamespace();
this.setName = thisEntry.getSetName();
break;
}
thisEntry = thisEntry.superClazz;
}
}
ClassCacheEntry> thisEntry = this.superClazz;
while (thisEntry != null) {
if (this.durableDelete == null && thisEntry.getDurableDelete() != null) {
this.durableDelete = thisEntry.getDurableDelete();
}
if (this.ttl == null && thisEntry.getTtl() != null) {
this.ttl = thisEntry.getTtl();
}
if (this.sendKey == null && thisEntry.getSendKey() != null) {
this.sendKey = thisEntry.getSendKey();
}
if (this.key == null && thisEntry.key != null) {
this.key = thisEntry.key;
}
thisEntry = thisEntry.superClazz;
}
}
private BinConfig getBinFromName(String name) {
if (this.classConfig == null || this.classConfig.getBins() == null) {
return null;
}
for (BinConfig thisBin: this.classConfig.getBins()) {
if (thisBin.getDerivedName().equals(name)) {
return thisBin;
}
}
return null;
}
private BinConfig getBinFromField(Field field) {
if (this.classConfig == null || this.classConfig.getBins() == null) {
return null;
}
for (BinConfig thisBin: this.classConfig.getBins()) {
if (thisBin.getField() != null && thisBin.getField().equals(field.getName())) {
return thisBin;
}
}
return null;
}
private BinConfig getBinFromGetter(String name) {
if (this.classConfig == null || this.classConfig.getBins() == null) {
return null;
}
for (BinConfig thisBin: this.classConfig.getBins()) {
if (thisBin.getGetter() != null && thisBin.getGetter().equals(name)) {
return thisBin;
}
}
return null;
}
private BinConfig getBinFromSetter(String name) {
if (this.classConfig == null || this.classConfig.getBins() == null) {
return null;
}
for (BinConfig thisBin: this.classConfig.getBins()) {
if (thisBin.getSetter() != null && thisBin.getSetter().equals(name)) {
return thisBin;
}
}
return null;
}
private void formOrdinalsFromValues() {
for (String thisValueName : this.values.keySet()) {
ValueType thisValue = this.values.get(thisValueName);
BinConfig binConfig = getBinFromName(thisValueName);
Integer ordinal = binConfig == null ? null : binConfig.getOrdinal();
if (ordinal == null) {
for (Annotation thisAnnotation : thisValue.getAnnotations()) {
if (thisAnnotation instanceof AerospikeOrdinal) {
ordinal = ((AerospikeOrdinal)thisAnnotation).value();
}
break;
}
}
if (ordinal != null) {
if (ordinals == null) {
ordinals = new HashMap<>();
fieldsWithOrdinals = new HashSet<>();
}
if (ordinals.containsKey(ordinal)) {
throw new AerospikeException(String.format("Class %s has multiple values with the ordinal of %d", clazz.getSimpleName(), ordinal));
}
ordinals.put(ordinal, thisValueName);
fieldsWithOrdinals.add(thisValueName);
}
}
if (ordinals != null) {
// The ordinals need to be valued from 1..
for (int i = 1; i <= ordinals.size(); i++) {
if (!ordinals.containsKey(i)) {
throw new AerospikeException(String.format("Class %s has %d values specifying ordinals. These should be 1..%d, but %d is missing",
clazz.getSimpleName(), ordinals.size(), ordinals.size(), i));
}
}
}
}
private void findConstructor() {
Constructor>[] constructors = clazz.getDeclaredConstructors();
if (constructors.length == 0) {
throw new AerospikeException("Class " + clazz.getSimpleName() + " has no constructors and hence cannot be mapped to Aerospike");
}
Constructor> desiredConstructor = null;
Constructor> noArgConstructor = null;
if (constructors.length == 1) {
desiredConstructor = constructors[0];
}
else {
for (Constructor> thisConstructor : constructors) {
if (thisConstructor.getParameters().length == 0) {
noArgConstructor = thisConstructor;
}
AerospikeConstructor aerospikeConstructor = thisConstructor.getAnnotation(AerospikeConstructor.class);
if (aerospikeConstructor != null) {
if (desiredConstructor != null) {
throw new AerospikeException("Class " + clazz.getSimpleName() + " has multiple constructors annotated with @AerospikeConstructor. Only one constructor can be so annotated.");
}
else {
desiredConstructor = thisConstructor;
}
}
}
}
if (desiredConstructor == null && noArgConstructor != null) {
constructorParamBins = new String[0];
desiredConstructor = noArgConstructor;
}
if (desiredConstructor == null) {
throw new AerospikeException("Class " + clazz.getSimpleName() + " has neither a no-arg constructor, nor a constructor annotated with @AerospikeConstructor so cannot be mapped to Aerospike.");
}
Parameter[] params = desiredConstructor.getParameters();
this.constructorParamBins = new String[params.length];
this.constructorParamDefaults = new Object[params.length];
Map allValues = new HashMap<>();
ClassCacheEntry> current = this;
while (current != null) {
allValues.putAll(current.values);
current = current.superClazz;
}
int count = 0;
for (Parameter thisParam : params) {
count++;
ParamFrom parameterDetails = thisParam.getAnnotation(ParamFrom.class);
if (parameterDetails == null) {
throw new AerospikeException("Class " + clazz.getSimpleName() + " has a preferred constructor of " + desiredConstructor.toString()+ ". However, parameter " + count +
" is not marked with a @ParamFrom annotation, and hence cannot be determined how to map to this from the bins in the record.");
}
String binName = parameterDetails.value();
// Validate that we have such a value
if (!allValues.containsKey(binName)) {
String valueList = String.join(",", values.keySet());
throw new AerospikeException("Class " + clazz.getSimpleName() + " has a preferred constructor of " + desiredConstructor.toString()+ ". However, parameter " + count +
" is mapped to bin \"" + binName + "\" which is not one of the values on the class, which are: " + valueList);
}
Class> type = thisParam.getType();
if (!type.isAssignableFrom(allValues.get(binName).getType())) {
throw new AerospikeException("Class " + clazz.getSimpleName() + " has a preferred constructor of " + desiredConstructor.toString()+ ". However, parameter " + count +
" is of type " + type + " but assigned from bin \"" + binName + "\" of type " +values.get(binName).getType()+ ". These types are incompatible.");
}
constructorParamBins[count-1] = binName;
constructorParamDefaults[count-1] = PrimitiveDefaults.getDefaultValue(thisParam.getType());
}
this.constructor = (Constructor) desiredConstructor;
this.constructor.setAccessible(true);
}
private PropertyDefinition getOrCreateProperty(String name, Map properties) {
PropertyDefinition thisProperty = properties.get(name);
if (thisProperty == null) {
thisProperty = new PropertyDefinition(name, mapper);
properties.put(name,thisProperty);
}
return thisProperty;
}
private void loadPropertiesFromClass(@NotNull Class> clazz, ClassConfig config) {
Map properties = new HashMap<>();
PropertyDefinition keyProperty = null;
KeyConfig keyConfig = config != null ? config.getKey() : null;
for (Method thisMethod : clazz.getDeclaredMethods()) {
String methodName = thisMethod.getName();
BinConfig getterConfig = getBinFromGetter(methodName);
BinConfig setterConfig = getBinFromSetter(methodName);
boolean isKey = false;
boolean isKeyViaConfig = keyConfig != null && (keyConfig.isGetter(methodName) || keyConfig.isSetter(methodName));
if (thisMethod.isAnnotationPresent(AerospikeKey.class) || isKeyViaConfig) {
if (keyProperty == null) {
keyProperty = new PropertyDefinition("_key_", mapper);
}
if (isKeyViaConfig) {
if (keyConfig.isGetter(methodName)) {
keyProperty.setGetter(thisMethod);
}
else {
keyProperty.setSetter(thisMethod);
}
}
else {
AerospikeKey key = thisMethod.getAnnotation(AerospikeKey.class);
if (key.setter()) {
keyProperty.setSetter(thisMethod);
}
else {
keyProperty.setGetter(thisMethod);
}
}
isKey = true;
}
if (thisMethod.isAnnotationPresent(AerospikeGetter.class) || getterConfig != null) {
String getterName = (getterConfig != null) ? getterConfig.getGetter() : thisMethod.getAnnotation(AerospikeGetter.class).name();
String name = ParserUtils.getInstance().get(ParserUtils.getInstance().get(getterName));
PropertyDefinition thisProperty = getOrCreateProperty(name, properties);
thisProperty.setGetter(thisMethod);
if (isKey) {
keyName = name;
}
}
if (thisMethod.isAnnotationPresent(AerospikeSetter.class) || setterConfig != null) {
String setterName = (setterConfig != null) ? setterConfig.getSetter() : thisMethod.getAnnotation(AerospikeSetter.class).name();
PropertyDefinition thisProperty = getOrCreateProperty(ParserUtils.getInstance().get(ParserUtils.getInstance().get(setterName)), properties);
thisProperty.setSetter(thisMethod);
}
}
if (keyProperty != null) {
keyProperty.validate(clazz.getName(), config, true);
if (key != null) {
throw new AerospikeException("Class " + clazz.getName() + " cannot have a more than one key");
}
AnnotatedType annotatedType = new AnnotatedType(config, keyProperty.getGetter());
TypeMapper typeMapper = TypeUtils.getMapper(keyProperty.getType(), annotatedType, this.mapper);
this.key = new ValueType.MethodValue(keyProperty, typeMapper, annotatedType);
}
for (String thisPropertyName : properties.keySet()) {
PropertyDefinition thisProperty = properties.get(thisPropertyName);
thisProperty.validate(clazz.getName(), config, false);
if (this.values.get(thisPropertyName) != null) {
throw new AerospikeException("Class " + clazz.getName() + " cannot define the mapped name " + thisPropertyName + " more than once");
}
AnnotatedType annotatedType = new AnnotatedType(config, thisProperty.getGetter());
TypeMapper typeMapper = TypeUtils.getMapper(thisProperty.getType(), annotatedType, this.mapper);
ValueType value = new ValueType.MethodValue(thisProperty, typeMapper, annotatedType);
values.put(thisPropertyName, value);
}
}
private void loadFieldsFromClass(Class> clazz, boolean mapAll, ClassConfig config) {
KeyConfig keyConfig = config != null ? config.getKey() : null;
String keyField = keyConfig == null ? null : keyConfig.getField();
for (Field thisField : clazz.getDeclaredFields()) {
boolean isKey = false;
BinConfig thisBin = getBinFromField(thisField);
if (thisField.isAnnotationPresent(AerospikeKey.class) || (!StringUtils.isBlank(keyField) && keyField.equals(thisField.getName()))) {
if (thisField.isAnnotationPresent(AerospikeExclude.class) || (thisBin != null && thisBin.isExcluded())) {
throw new AerospikeException("Class " + clazz.getName() + " cannot have a field which is both a key and excluded.");
}
if (key != null) {
throw new AerospikeException("Class " + clazz.getName() + " cannot have a more than one key");
}
AnnotatedType annotatedType = new AnnotatedType(config, thisField);
TypeMapper typeMapper = TypeUtils.getMapper(thisField.getType(), annotatedType, this.mapper);
this.key = new ValueType.FieldValue(thisField, typeMapper, annotatedType);
isKey = true;
}
if (thisField.isAnnotationPresent(AerospikeExclude.class) || (thisBin != null && thisBin.isExcluded() != null && thisBin.isExcluded())) {
// This field should be excluded from being stored in the database. Even keys must be stored
continue;
}
if (this.mapAll || thisField.isAnnotationPresent(AerospikeBin.class) || thisBin != null) {
// This field needs to be mapped
thisField.setAccessible(true);
AerospikeBin bin = thisField.getAnnotation(AerospikeBin.class);
String binName = bin == null ? null : ParserUtils.getInstance().get(bin.name());
if (thisBin != null && !StringUtils.isBlank(thisBin.getDerivedName())) {
binName = thisBin.getDerivedName();
}
String name;
if (StringUtils.isBlank(binName)) {
name = thisField.getName();
}
else {
name = binName;
}
if (isKey) {
this.keyName = name;
}
if (this.values.get(name) != null) {
throw new AerospikeException("Class " + clazz.getName() + " cannot define the mapped name " + name + " more than once");
}
AnnotatedType annotatedType = new AnnotatedType(config, thisField);
TypeMapper typeMapper = TypeUtils.getMapper(thisField.getType(), annotatedType, this.mapper);
ValueType valueType = new ValueType.FieldValue(thisField, typeMapper, annotatedType);
values.put(name, valueType);
}
}
}
public Object translateKeyToAerospikeKey(Object key) {
return this.key.getTypeMapper().toAerospikeFormat(key);
}
private Object _getKey(Object object) throws ReflectiveOperationException {
if (this.key != null) {
return this.translateKeyToAerospikeKey(this.key.get(object));
}
else if (superClazz != null) {
return this.superClazz._getKey(object);
}
return null;
}
public Object getKey(Object object) {
try {
Object key = this._getKey(object);
if (key == null) {
throw new AerospikeException("Null key from annotated object of class " + this.clazz.getSimpleName() + ". Did you forget an @AerospikeKey annotation?");
}
return key;
}
catch (ReflectiveOperationException re) {
throw new AerospikeException(re);
}
}
private void _setKey(Object object, Object value) throws ReflectiveOperationException {
if (this.key != null) {
this.key.set(object, this.key.getTypeMapper().fromAerospikeFormat(value));
}
else if (superClazz != null) {
this.superClazz._setKey(object, value);
}
}
public void setKey(Object object, Object value) {
try {
this._setKey(object, value);
}
catch (ReflectiveOperationException re) {
throw new AerospikeException(re);
}
}
public String getNamespace() {
return namespace;
}
public String getSetName() {
return setName;
}
public Integer getTtl() {
return ttl;
}
public Boolean getSendKey() {
return sendKey;
}
public Boolean getDurableDelete() {
return durableDelete;
}
private boolean contains(String[] names, String thisName) {
if (names == null || names.length == 0) {
return true;
}
if (thisName == null) {
return false;
}
for (String aName : names) {
if (thisName.equals(aName)) {
return true;
}
}
return false;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public Bin[] getBins(Object instance, boolean allowNullBins, String[] binNames) {
try {
Bin[] bins = new Bin[this.binCount];
int index = 0;
ClassCacheEntry thisClass = this;
while (thisClass != null) {
Set keys = thisClass.values.keySet();
for (String name : keys) {
if (contains(binNames, name)) {
ValueType value = (ValueType) thisClass.values.get(name);
Object javaValue = value.get(instance);
Object aerospikeValue = value.getTypeMapper().toAerospikeFormat(javaValue);
if (aerospikeValue != null || allowNullBins) {
if (aerospikeValue instanceof TreeMap, ?>) {
TreeMap,?> treeMap = (TreeMap,?>)aerospikeValue;
bins[index++] = new Bin(name, new ArrayList(treeMap.entrySet()), MapOrder.KEY_ORDERED);
}
else {
bins[index++] = new Bin(name, aerospikeValue);
}
}
}
}
thisClass = thisClass.superClazz;
}
if (index != this.binCount) {
bins = Arrays.copyOf(bins, index);
}
return bins;
}
catch (ReflectiveOperationException ref) {
throw new AerospikeException(ref);
}
}
public Map getMap(Object instance, boolean needsType) {
try {
Map results = new HashMap<>();
ClassCacheEntry> thisClass = this;
if (needsType) {
results.put(TYPE_NAME, this.getShortenedClassName());
}
while (thisClass != null) {
for (String name : thisClass.values.keySet()) {
ValueType value = thisClass.values.get(name);
Object javaValue = value.get(instance);
Object aerospikeValue = value.getTypeMapper().toAerospikeFormat(javaValue);
results.put(name, aerospikeValue);
}
thisClass = thisClass.superClazz;
}
return results;
}
catch (ReflectiveOperationException ref) {
throw new AerospikeException(ref);
}
}
private void addDataFromValueName(String name, Object instance, ClassCacheEntry> thisClass, List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy