com.antgroup.tugraph.ogm.metadata.ClassInfo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tugraph-db-ogm-core Show documentation
Show all versions of tugraph-db-ogm-core Show documentation
TuGraph-DB-OGM is an Object Graph Mapping Library for TuGraph.
The newest version!
/*
* Copyright (c) 2002-2022 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.antgroup.tugraph.ogm.metadata;
import static java.util.stream.Collectors.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import com.antgroup.tugraph.ogm.annotation.*;
import com.antgroup.tugraph.ogm.autoindex.AutoIndexManager;
import com.antgroup.tugraph.ogm.driver.TypeSystem;
import com.antgroup.tugraph.ogm.exception.core.InvalidPropertyFieldException;
import com.antgroup.tugraph.ogm.exception.core.MappingException;
import com.antgroup.tugraph.ogm.exception.core.MetadataException;
import com.antgroup.tugraph.ogm.id.IdStrategy;
import com.antgroup.tugraph.ogm.id.InternalIdStrategy;
import com.antgroup.tugraph.ogm.id.UuidStrategy;
import com.antgroup.tugraph.ogm.support.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maintains object to graph mapping details at the class (type) level
* The ClassInfo object is used to maintain mappings from Java Types->Neo4j Labels
* thereby allowing the correct labels to be applied to new nodes when they
* are persisted.
* The ClassInfo object also maintains a map of FieldInfo and MethodInfo objects
* that maintain the appropriate information for mapping Java class attributes to Neo4j
* node properties / paths (node)-[:relationship]->(node), via field or method
* accessors respectively.
* Given a type hierarchy, the ClassInfo object guarantees that for any type in that
* hierarchy, the labels associated with that type will include the labels for
* all its superclass and interface types as well. This is to avoid the need to iterate
* through the ClassInfo hierarchy to recover label information.
*
* @author Vince Bickers
* @author Luanne Misquitta
* @author Mark Angrish
* @author Michael J. Simons
* @author Torsten Kuhnhenne
* @author Nicolas Labrot
*/
public class ClassInfo {
private static final Logger LOGGER = LoggerFactory.getLogger(ClassInfo.class);
private final List directSubclasses = new ArrayList<>();
private volatile Set allSubclasses;
private final List directInterfaces = new ArrayList<>();
private final List directImplementingClasses = new ArrayList<>();
/**
* Indirect super classes will only be filled for Kotlin classes that make use of Kotlin's
* "Implementation by Delegation". Find a
* more in depth of the inner workings
* here
* (and feel free to replace the above medium link with a real spec doc).
*/
private final List indirectSuperClasses = new ArrayList<>();
private final String className;
private final boolean isInterface;
private final boolean isAbstract;
private final boolean isEnum;
private ClassInfo directSuperclass;
private String directSuperclassName;
private String neo4jName;
private final FieldsInfo fieldsInfo;
private final MethodsInfo methodsInfo;
private final AnnotationsInfo annotationsInfo;
private final InterfacesInfo interfacesInfo;
private final Class> cls;
private final Map> iterableFieldsForType = new HashMap<>();
private final Map fieldInfoFields = new ConcurrentHashMap<>();
private volatile Map propertyFields;
private volatile Map indexFields;
private volatile Collection requiredFields;
private volatile Collection compositeIndexes;
private volatile Optional identityField;
private volatile Optional versionField;
private volatile Optional primaryIndexField;
private volatile FieldInfo labelField = null;
private volatile boolean labelFieldMapped = false;
private volatile Optional postLoadMethod;
private volatile Collection staticLabels;
private volatile Set relationshipFields;
private volatile Optional endNodeReader;
private volatile Optional startNodeReader;
private Class extends IdStrategy> idStrategyClass;
private IdStrategy idStrategy;
public ClassInfo(Class> cls, TypeSystem typeSystem) {
this(cls, null, typeSystem);
}
/**
* @param cls The type of this class
* @param parent Will be filled if containing class is a Kotlin class and this class is the type of a Kotlin delegate.
* @param typeSystem The typesystem in use
*/
private ClassInfo(Class> cls, Field parent, TypeSystem typeSystem) {
this.cls = cls;
final int modifiers = cls.getModifiers();
this.isInterface = Modifier.isInterface(modifiers);
this.isAbstract = Modifier.isAbstract(modifiers);
this.isEnum = ClassUtils.isEnum(cls);
this.className = cls.getName();
if (cls.getSuperclass() != null) {
this.directSuperclassName = cls.getSuperclass().getName();
}
this.interfacesInfo = new InterfacesInfo(cls);
this.fieldsInfo = new FieldsInfo(this, cls, parent, typeSystem);
this.methodsInfo = new MethodsInfo(cls, parent);
this.annotationsInfo = new AnnotationsInfo(cls);
if (isRelationshipEntity() && labelFieldOrNull() != null) {
throw new MappingException(
String.format("'%s' is a relationship entity. The @Labels annotation can't be applied to " +
"relationship entities.", name()));
}
for (FieldInfo fieldInfo : fieldsInfo().fields()) {
if (fieldInfo.hasAnnotation(Property.class) && fieldInfo.hasCompositeConverter()) {
throw new MappingException(
String.format("'%s' has both @Convert and @Property annotations applied to the field '%s'",
name(), fieldInfo.getName()));
}
}
if (KotlinDetector.isKotlinType(cls)) {
this.inspectLocalDelegates(typeSystem);
}
}
private void inspectLocalDelegates(TypeSystem typeSystem) {
for (Field field : this.cls.getDeclaredFields()) {
if (!isKotlinDelegate(field)) {
continue;
}
ClassInfo indirectSuperClass = new ClassInfo(field.getType(), field, typeSystem);
this.extend(indirectSuperClass);
this.indirectSuperClasses.add(indirectSuperClass);
}
}
private static boolean isKotlinDelegate(Field field) {
return field.isSynthetic() && field.getName().startsWith("$$delegate_");
}
void extend(ClassInfo classInfo) {
this.interfacesInfo.append(classInfo.interfacesInfo());
this.fieldsInfo.append(classInfo.fieldsInfo());
this.methodsInfo.append(classInfo.methodsInfo());
}
/**
* Connect this class to a subclass.
*
* @param subclass the subclass
*/
void addSubclass(ClassInfo subclass) {
if (subclass.directSuperclass != null && subclass.directSuperclass != this) {
throw new RuntimeException(
subclass.className + " has two superclasses: " + subclass.directSuperclass.className + ", "
+ this.className);
}
subclass.directSuperclass = this;
this.directSubclasses.add(subclass);
}
public String name() {
return className;
}
String simpleName() {
return deriveSimpleName(this.cls);
}
public static String deriveSimpleName(Class> clazz) {
String className = clazz.getName();
return className.substring(className.lastIndexOf('.') + 1);
}
public ClassInfo directSuperclass() {
return directSuperclass;
}
/**
*
* Retrieves the static labels that are applied to nodes in the database. If the class' instances are persisted by
* a relationship instead of a node then this method returns an empty collection.
*
*
* Note that this method returns only the static labels. A node entity instance may declare additional labels
* managed at runtime by using the @Labels annotation on a collection field, therefore the full set of labels to be
* mapped to a node will be the static labels, in addition to any labels declared by the backing field of an
* {@link Labels} annotation.
*
*
* @return A {@link Collection} of all the static labels that apply to the node or an empty list if there aren't
* any, never null
*/
public Collection staticLabels() {
Collection knownStaticLabels = this.staticLabels;
if (knownStaticLabels == null) {
synchronized (this) {
knownStaticLabels = this.staticLabels;
if (knownStaticLabels == null) {
this.staticLabels = Collections.unmodifiableCollection(collectLabels());
knownStaticLabels = this.staticLabels;
}
}
}
return knownStaticLabels;
}
public String neo4jName() {
if (neo4jName == null) {
AnnotationInfo annotationInfo = annotationsInfo.get(NodeEntity.class);
if (annotationInfo != null) {
neo4jName = annotationInfo.get(NodeEntity.LABEL, simpleName());
return neo4jName;
}
annotationInfo = annotationsInfo.get(RelationshipEntity.class);
if (annotationInfo != null) {
neo4jName = annotationInfo.get(RelationshipEntity.TYPE, simpleName().toUpperCase());
return neo4jName;
}
if (!isAbstract) {
neo4jName = simpleName();
}
}
return neo4jName;
}
private Collection collectLabels() {
List labels = new ArrayList<>();
if (!isAbstract || annotationsInfo.get(NodeEntity.class) != null) {
labels.add(neo4jName());
}
if (directSuperclass != null && !"java.lang.Object".equals(directSuperclass.className)) {
labels.addAll(directSuperclass.collectLabels());
}
for (ClassInfo interfaceInfo : directInterfaces()) {
labels.addAll(interfaceInfo.collectLabels());
}
for (ClassInfo indirectSuperClass : indirectSuperClasses) {
labels.addAll(indirectSuperClass.collectLabels());
}
return labels;
}
public List directSubclasses() {
return directSubclasses;
}
/**
* @return A list of all implementing and extending subclasses.
* @since 3.1.20
*/
public Collection allSubclasses() {
Set computedSubclasses = this.allSubclasses;
if (computedSubclasses == null) {
synchronized (this) {
computedSubclasses = this.allSubclasses;
if (computedSubclasses == null) {
this.allSubclasses = computeSubclasses();
computedSubclasses = this.allSubclasses;
}
}
}
return computedSubclasses;
}
private Set computeSubclasses() {
Set computedSubclasses = new HashSet<>();
for (ClassInfo classInfo : this.directSubclasses()) {
computedSubclasses.add(classInfo);
computedSubclasses.addAll(classInfo.allSubclasses());
}
for (ClassInfo classInfo : this.directImplementingClasses()) {
computedSubclasses.add(classInfo);
computedSubclasses.addAll(classInfo.allSubclasses());
}
return Collections.unmodifiableSet(computedSubclasses);
}
List directImplementingClasses() {
return directImplementingClasses;
}
List directInterfaces() {
return directInterfaces;
}
InterfacesInfo interfacesInfo() {
return interfacesInfo;
}
public Collection annotations() {
return annotationsInfo.list();
}
public boolean isInterface() {
return isInterface;
}
public boolean isEnum() {
return isEnum;
}
public AnnotationsInfo annotationsInfo() {
return annotationsInfo;
}
String superclassName() {
return directSuperclassName;
}
public FieldsInfo fieldsInfo() {
return fieldsInfo;
}
MethodsInfo methodsInfo() {
return methodsInfo;
}
public FieldInfo identityFieldOrNull() {
return getOrComputeIdentityField().orElse(null);
}
/**
* The identity field is a field annotated with @NodeId, or if none exists, a field
* of type Long called 'id'
*
* @return A {@link FieldInfo} object representing the identity field never null
* @throws MappingException if no identity field can be found
*/
public FieldInfo identityField() {
return getOrComputeIdentityField()
.orElseThrow(() -> new MetadataException("No internal identity field found for class: " + this.className));
}
private Optional getOrComputeIdentityField() {
Optional result = this.identityField;
if (result == null) {
synchronized (this) {
result = this.identityField;
if (result == null) {
// Didn't want to add yet another method related to determining the identy field
// so the actual resolving of the field inside the Double-checked locking here
// has been inlined.
Collection identityFields = getFieldInfos(FieldInfo::isInternalIdentity);
if (identityFields.size() == 1) {
this.identityField = Optional.of(identityFields.iterator().next());
} else if (identityFields.size() > 1) {
throw new MetadataException("Expected exactly one internal identity field (@Id with " +
"InternalIdStrategy), found " + identityFields.size() + " " + identityFields);
} else {
this.identityField = fieldsInfo.fields().stream()
.filter(f -> "id".equals(f.getName()))
.filter(f -> "java.lang.Long".equals(f.getTypeDescriptor()))
.findFirst();
}
result = this.identityField;
}
}
}
return result;
}
public boolean hasIdentityField() {
return getOrComputeIdentityField().isPresent();
}
Collection getFieldInfos(Predicate predicate) {
return fieldsInfo().fields().stream()
.filter(predicate)
.collect(Collectors.toSet());
}
/**
* The label field is an optional field annotated with @Labels.
*
* @return A {@link FieldInfo} object representing the label field. Optionally null
*/
public FieldInfo labelFieldOrNull() {
if (labelFieldMapped) {
return labelField;
}
if (!labelFieldMapped) {
for (FieldInfo fieldInfo : fieldsInfo().fields()) {
if (fieldInfo.isLabelField()) {
if (!fieldInfo.isIterable()) {
throw new MappingException(String.format(
"Field '%s' in class '%s' includes the @Labels annotation, however this field is not a " +
"type of collection.", fieldInfo.getName(), this.name()));
}
labelFieldMapped = true;
labelField = fieldInfo;
return labelField;
}
}
labelFieldMapped = true;
}
return null;
}
public boolean isRelationshipEntity() {
for (AnnotationInfo info : annotations()) {
if (info.getName().equals(RelationshipEntity.class.getName())) {
return true;
}
}
return false;
}
/**
* A property field is any field annotated with @Property, or any field that can be mapped to a
* node property. The identity field is not a property field.
*
* @return A Collection of FieldInfo objects describing the classInfo's property fields
* @throws InvalidPropertyFieldException if the recognized property fields contain a field that is not
* actually persistable as property.
*/
public Collection propertyFields() {
return getOrComputePropertyFields().values();
}
/**
* Finds the property field with a specific property name from the ClassInfo's property fields
* Note that this method does not allow for property names with differing case. //TODO
*
* @param propertyName the propertyName of the field to find
* @return A FieldInfo object describing the required property field, or null if it doesn't exist.
* @throws InvalidPropertyFieldException if the recognized property fields contain a field that is not
* actually persistable as property.
*/
public FieldInfo propertyField(String propertyName) {
return propertyName == null ? null : getOrComputePropertyFields().get(propertyName);
}
private Map getOrComputePropertyFields() {
Map result = this.propertyFields;
if (result == null) {
synchronized (this) {
result = this.propertyFields;
if (result == null) {
Collection fields = fieldsInfo().fields();
FieldInfo optionalIdentityField = identityFieldOrNull();
Map intermediateFieldMap = new HashMap<>(fields.size());
for (FieldInfo fieldInfo : fields) {
if (fieldInfo == optionalIdentityField || fieldInfo.isLabelField() || fieldInfo.hasAnnotation(
StartNode.class) || fieldInfo.hasAnnotation(EndNode.class)) {
continue;
}
if (!fieldInfo.getAnnotations().has(Property.class)) {
// If a field is not marked explicitly as a property but is persistable as such, add it.
if (fieldInfo.persistableAsProperty()) {
intermediateFieldMap.put(fieldInfo.property(), fieldInfo);
}
} else if (fieldInfo.persistableAsProperty()) {
// If it is marked as a property, then it should be persistable as such
intermediateFieldMap.put(fieldInfo.property(), fieldInfo);
} else {
// Otherwise, throw a fitting exception
throw new InvalidPropertyFieldException(fieldInfo);
}
}
this.propertyFields = Collections.unmodifiableMap(intermediateFieldMap);
result = this.propertyFields;
}
}
}
return result;
}
/**
* Finds the property field with a specific field name from the ClassInfo's property fields
*
* @param propertyName the propertyName of the field to find
* @return A FieldInfo object describing the required property field, or null if it doesn't exist.
*/
public FieldInfo propertyFieldByName(String propertyName) {
for (FieldInfo fieldInfo : propertyFields()) {
if (fieldInfo.getName().equalsIgnoreCase(propertyName)) {
return fieldInfo;
}
}
return null;
}
/**
* A relationship field is any field annotated with @Relationship, or any field that cannot be mapped to a
* node property. The identity field is not a relationship field.
*
* @return A Collection of FieldInfo objects describing the classInfo's relationship fields
*/
public Collection relationshipFields() {
Collection result = this.relationshipFields;
if (result == null) {
synchronized (this) {
result = this.relationshipFields;
if (result == null) {
FieldInfo optionalIdentityField = identityFieldOrNull();
Set identifiedRelationshipFields = new HashSet<>();
for (FieldInfo fieldInfo : fieldsInfo().fields()) {
if (fieldInfo == optionalIdentityField) {
continue;
}
if (fieldInfo.getAnnotations().has(Relationship.class)) {
identifiedRelationshipFields.add(fieldInfo);
} else if (!fieldInfo.persistableAsProperty()) {
identifiedRelationshipFields.add(fieldInfo);
}
}
this.relationshipFields = Collections.unmodifiableSet(identifiedRelationshipFields);
result = this.relationshipFields;
}
}
}
return result;
}
/**
* Finds the relationship field with a specific name from the ClassInfo's relationship fields
*
* @param relationshipName the relationshipName of the field to find
* @return A FieldInfo object describing the required relationship field, or null if it doesn't exist.
*/
public FieldInfo relationshipField(String relationshipName) {
for (FieldInfo fieldInfo : relationshipFields()) {
if (fieldInfo.relationship().equalsIgnoreCase(relationshipName)) {
return fieldInfo;
}
}
return null;
}
/**
* Finds the relationship field with a specific name and direction from the ClassInfo's relationship fields
*
* @param relationshipName the relationshipName of the field to find
* @param relationshipDirection the direction of the relationship represented by a string
* @param strict if true, does not infer relationship type but looks for it in the @Relationship annotation. Null if missing. If false, infers relationship type from FieldInfo
* @return A FieldInfo object describing the required relationship field, or null if it doesn't exist.
*/
public FieldInfo relationshipField(String relationshipName, Relationship.Direction relationshipDirection, boolean strict) {
for (FieldInfo fieldInfo : relationshipFields()) {
String relationship = strict ? fieldInfo.relationshipTypeAnnotation() : fieldInfo.relationship();
if (relationshipName.equalsIgnoreCase(relationship)) {
Relationship.Direction declaredDirection = fieldInfo.relationshipDirectionOrDefault(Relationship.Direction.OUTGOING);
if (isActualDirectionCompatibleWithDeclaredDirection(relationshipDirection, declaredDirection)) {
return fieldInfo;
}
}
}
return null;
}
/**
* Finds all relationship fields with a specific name and direction from the ClassInfo's relationship fields
*
* @param relationshipName the relationshipName of the field to find
* @param relationshipDirection the direction of the relationship
* @param strict if true, does not infer relationship type but looks for it in the @Relationship annotation. Null if missing. If false, infers relationship type from FieldInfo
* @return Set of FieldInfo objects describing the required relationship field, or empty set if it doesn't exist.
*/
public Set candidateRelationshipFields(String relationshipName, Relationship.Direction relationshipDirection, boolean strict) {
Set candidateFields = new HashSet<>();
for (FieldInfo fieldInfo : relationshipFields()) {
String relationship = strict ? fieldInfo.relationshipTypeAnnotation() : fieldInfo.relationship();
if (relationshipName.equalsIgnoreCase(relationship)) {
Relationship.Direction declaredDirection = fieldInfo.relationshipDirectionOrDefault(Relationship.Direction.OUTGOING);
if (isActualDirectionCompatibleWithDeclaredDirection(relationshipDirection, declaredDirection)) {
candidateFields.add(fieldInfo);
}
}
}
return candidateFields;
}
/**
* Finds the relationship field with a specific property name from the ClassInfo's relationship fields
*
* @param fieldName the name of the field
* @return A FieldInfo object describing the required relationship field, or null if it doesn't exist.
*/
public FieldInfo relationshipFieldByName(String fieldName) {
for (FieldInfo fieldInfo : relationshipFields()) {
if (fieldInfo.getName().equalsIgnoreCase(fieldName)) {
return fieldInfo;
}
}
return null;
}
public Field getField(FieldInfo fieldInfo) {
Field field = fieldInfoFields.get(fieldInfo);
if (field != null) {
return field;
}
try {
field = cls.getDeclaredField(fieldInfo.getName());
fieldInfoFields.put(fieldInfo, field);
return field;
} catch (NoSuchFieldException e) {
if (directSuperclass() != null) {
field = directSuperclass().getField(fieldInfo);
fieldInfoFields.put(fieldInfo, field);
return field;
} else {
throw new RuntimeException(
"Field " + fieldInfo.getName() + " not found in class " + name() + " or any of its superclasses");
}
}
}
/**
* Find all FieldInfos for the specified ClassInfo whose type matches the supplied fieldType
*
* @param fieldType The field type to look for
* @return A {@link List} of {@link FieldInfo} objects that are of the given type, never null
*/
public List findFields(Class> fieldType) {
String fieldSignature = fieldType.getName();
Predicate matchesType = f -> f.getTypeDescriptor().equals(fieldSignature);
return fieldsInfo().fields().stream().filter(matchesType).collect(toList());
}
/**
* Find all FieldInfos for the specified ClassInfo which have the specified annotation
*
* @param annotation The annotation
* @return A {@link List} of {@link FieldInfo} objects that are of the given type, never null
*/
public List findFields(String annotation) {
Predicate hasAnnotation = f -> f.hasAnnotation(annotation);
return fieldsInfo().fields().stream().filter(hasAnnotation).collect(toList());
}
/**
* Retrieves a {@link List} of {@link FieldInfo} representing all of the fields that can be iterated over
* using a "foreach" loop.
*
* @return {@link List} of {@link FieldInfo}
*/
public List findIterableFields() {
Predicate isIterable = f -> {
// The actual call to getField might throw an exception.
// While FieldInfo#type() should also return the type,
// ClassInfo#getField has side-effects which I cannot judge
// atm, so better keep it here
// and wrap the predicate in an exception below.
Class type = getField(f).getType();
return type.isArray() || Iterable.class.isAssignableFrom(type);
};
// See comment inside predicate regarding this exception.
try {
return fieldsInfo().fields().stream()
.filter(isIterable).collect(toList());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Finds all fields whose type is equivalent to Array<X> or assignable from Iterable<X>
* where X is the generic parameter type of the Array or Iterable
*
* @param iteratedType the type of iterable
* @return {@link List} of {@link MethodInfo}, never null
*/
public List findIterableFields(Class iteratedType) {
if (iterableFieldsForType.containsKey(iteratedType)) {
return iterableFieldsForType.get(iteratedType);
}
String typeSignature = iteratedType.getName();
String arrayOfTypeSignature = typeSignature + "[]";
Predicate isIterableOfType = f -> {
String fieldType = f.getTypeDescriptor();
boolean isMatchingArray =
f.isArray() && (fieldType.equals(arrayOfTypeSignature) || f.isParameterisedTypeOf(iteratedType));
boolean isMatchingIterable =
f.isIterable() && (fieldType.equals(typeSignature) || f.isParameterisedTypeOf(iteratedType));
return isMatchingArray || isMatchingIterable;
};
return fieldsInfo().fields().stream()
.filter(isIterableOfType).collect(toList());
}
/**
* Finds all fields whose type is equivalent to Array<X> or assignable from Iterable<X>
* where X is the generic parameter type of the Array or Iterable and the relationship type backing this iterable is "relationshipType"
*
* @param iteratedType the type of iterable
* @param relationshipType the relationship type
* @param relationshipDirection the relationship direction
* @param strict if true, does not infer relationship type but looks for it in the @Relationship annotation. Null if missing. If false, infers relationship type from FieldInfo
* @return {@link List} of {@link MethodInfo}, never null
*/
public List findIterableFields(Class> iteratedType, String relationshipType, Relationship.Direction relationshipDirection, boolean strict) {
List iterableFields = new ArrayList<>();
for (FieldInfo fieldInfo : findIterableFields(iteratedType)) {
String relationship = strict ? fieldInfo.relationshipTypeAnnotation() : fieldInfo.relationship();
if (relationshipType.equals(relationship)) {
Relationship.Direction declaredDirection = fieldInfo.relationshipDirectionOrDefault(Relationship.Direction.OUTGOING);
if (isActualDirectionCompatibleWithDeclaredDirection(relationshipDirection, declaredDirection)) {
iterableFields.add(fieldInfo);
}
}
}
return iterableFields;
}
private static boolean isActualDirectionCompatibleWithDeclaredDirection(Relationship.Direction actual, Relationship.Direction declared) {
return ((declared == Relationship.Direction.INCOMING || declared == Relationship.Direction.UNDIRECTED) && actual == Relationship.Direction.INCOMING) ||
(declared != Relationship.Direction.INCOMING && actual == Relationship.Direction.OUTGOING);
}
public boolean isTransient() {
return annotationsInfo.get(Transient.class) != null;
}
public boolean isAbstract() {
return isAbstract;
}
/**
* Returns true if this classInfo is in the subclass hierarchy of b, or if this classInfo is the same as b, false otherwise
*
* @param classInfo the classInfo at the toplevel of a type hierarchy to search through
* @return true if this classInfo is in the subclass hierarchy of classInfo, false otherwise
*/
boolean isSubclassOf(ClassInfo classInfo) {
if (classInfo == null) {
return false;
}
if (this == classInfo) {
return true;
}
for (ClassInfo subclass : classInfo.directSubclasses()) {
if (isSubclassOf(subclass)) {
return true;
}
}
return this.indirectSuperClasses.stream()
.anyMatch(c -> c.getUnderlyingClass() == classInfo.getUnderlyingClass());
}
/**
* Get the underlying class represented by this ClassInfo
*
* @return the underlying class or null if it cannot be determined
*/
public Class> getUnderlyingClass() {
return cls;
}
/**
* Gets the class of the type parameter description of the entity related to this.
* The match is done based on the following-
* 2. Look for a field explicitly annotated with @Relationship for a type and implied direction
* 4. Look for a field with name derived from the relationship type for the given direction
*
* @param relationshipType the relationship type
* @param relationshipDirection the relationship direction
* @return class of the type parameter descriptor or null if it could not be determined
*/
Class> getTypeParameterDescriptorForRelationship(String relationshipType, Relationship.Direction relationshipDirection) {
final boolean STRICT_MODE = true; //strict mode for matching methods and fields, will only look for explicit annotations
final boolean INFERRED_MODE = false; //inferred mode for matching methods and fields, will infer the relationship type from the getter/setter/property
try {
FieldInfo fieldInfo = relationshipField(relationshipType, relationshipDirection, STRICT_MODE);
if (fieldInfo != null && fieldInfo.getTypeDescriptor() != null) {
return DescriptorMappings.getType(fieldInfo.getTypeDescriptor());
}
if (relationshipDirection != Relationship.Direction.INCOMING) { //we always expect an annotation for INCOMING
fieldInfo = relationshipField(relationshipType, relationshipDirection, INFERRED_MODE);
if (fieldInfo != null && fieldInfo.getTypeDescriptor() != null) {
return DescriptorMappings.getType(fieldInfo.getTypeDescriptor());
}
}
} catch (RuntimeException e) {
LOGGER.debug("Could not get {} class type for relationshipType {} and relationshipDirection {} ", className,
relationshipType, relationshipDirection);
}
return null;
}
/**
* @return If this class contains any fields/properties annotated with @Index.
*/
public boolean containsIndexes() {
return !(getIndexFields().isEmpty() && getCompositeIndexes().isEmpty());
}
/**
* @return The FieldInfo
s representing the Indexed fields in this class.
*/
public Collection getIndexFields() {
Map result = this.indexFields;
if (result == null) {
synchronized (this) {
result = this.indexFields;
if (result == null) {
Map indexes = new HashMap<>();
Field[] declaredFields = cls.getDeclaredFields();
for (FieldInfo fieldInfo : fieldsInfo().fields()) {
if (isDeclaredField(declaredFields, fieldInfo.getName()) &&
(fieldInfo.hasAnnotation(Index.class) || fieldInfo.hasAnnotation(Id.class))) {
String propertyValue = fieldInfo.property();
if (fieldInfo.hasAnnotation(Property.class.getName())) {
propertyValue = fieldInfo.property();
}
indexes.put(propertyValue, fieldInfo);
}
}
this.indexFields = Collections.unmodifiableMap(indexes);
result = this.indexFields;
}
}
}
return result.values();
}
private static boolean isDeclaredField(Field[] declaredFields, String name) {
for (Field field : declaredFields) {
if (field.getName().equals(name)) {
return true;
}
}
return false;
}
public Collection getCompositeIndexes() {
Collection result = this.compositeIndexes;
if (result == null) {
synchronized (this) {
result = this.compositeIndexes;
if (result == null) {
CompositeIndex[] annotations = cls.getDeclaredAnnotationsByType(CompositeIndex.class);
List intermediateResult = new ArrayList<>(annotations.length);
for (CompositeIndex annotation : annotations) {
String[] properties =
annotation.value().length > 0 ? annotation.value() : annotation.properties();
if (properties.length < 1) {
throw new MetadataException("Incorrect CompositeIndex definition on " + className +
". Provide at least 1 property");
}
for (String property : properties) {
// Determine the original field in case the user uses a MapCompositeConverter.
Matcher m = AutoIndexManager.COMPOSITE_KEY_MAP_COMPOSITE_PATTERN.matcher(property);
if (m.matches()) {
property = m.group(1);
}
FieldInfo fieldInfo = propertyField(property);
if (fieldInfo == null) {
throw new MetadataException(
"Incorrect CompositeIndex definition on " + className + ". Property " +
property + " does not exists.");
}
}
intermediateResult.add(annotation);
}
this.compositeIndexes = Collections.unmodifiableList(intermediateResult);
result = this.compositeIndexes;
}
}
}
return result;
}
public FieldInfo primaryIndexField() {
return getOrComputePrimaryIndexField().orElse(null);
}
private Optional getOrComputePrimaryIndexField() {
Optional result = this.primaryIndexField;
if (result == null) {
synchronized (this) {
result = this.primaryIndexField;
if (result == null) {
Optional potentialPrimaryIndexField = Optional.empty();
Collection primaryIndexFields = getFieldInfos(this::isPrimaryIndexField);
if (primaryIndexFields.size() > 1) {
throw new MetadataException(
"Only one @Id / @Index(primary=true, unique=true) annotation is allowed in a class hierarchy. Please check annotations in the class "
+ name() + " or its parents");
} else if (!primaryIndexFields.isEmpty()) {
FieldInfo selectedField = primaryIndexFields.iterator().next();
AnnotationInfo generatedValueAnnotation = selectedField.getAnnotations().get(GeneratedValue.class);
if (generatedValueAnnotation != null) {
// Here's a funny hidden side effect I wasn't able to refactor out with a clear idea during
// rethinking the whole collection of synchronized blocks :(
GeneratedValue value = (GeneratedValue) generatedValueAnnotation.getAnnotation();
idStrategyClass = value.strategy();
try {
idStrategy = idStrategyClass.getDeclaredConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
LOGGER.debug("Could not instantiate {}. Expecting this to be registered manually.", idStrategyClass);
}
}
potentialPrimaryIndexField = Optional.of(selectedField);
}
result = validateIdGenerationConfigFor(potentialPrimaryIndexField);
}
}
}
return result;
}
private Optional validateIdGenerationConfigFor(Optional potentialPrimaryIndexField) {
fieldsInfo().fields().forEach(info -> {
if (info.hasAnnotation(GeneratedValue.class) && !info.hasAnnotation(Id.class)) {
throw new MetadataException(
"The type of @Generated field in class " + className + " must be also annotated with @Id.");
}
});
if (UuidStrategy.class.equals(idStrategyClass)) {
potentialPrimaryIndexField.ifPresent(selectedField -> {
if (!(selectedField.isTypeOf(UUID.class) || selectedField.isTypeOf(String.class))) {
throw new MetadataException(
"The type of " + selectedField.getName() + " in class " + className
+ " must be of type java.lang.UUID or java.lang.String because it has an UUID generation strategy.");
}
});
}
return potentialPrimaryIndexField;
}
public boolean hasPrimaryIndexField() {
return getOrComputePrimaryIndexField().isPresent();
}
private boolean isPrimaryIndexField(FieldInfo fieldInfo) {
boolean hasIdAnnotation = fieldInfo.hasAnnotation(Id.class);
boolean hasStrategyOtherThanInternal = !fieldInfo.hasAnnotation(GeneratedValue.class)
|| !((GeneratedValue) fieldInfo.getAnnotations().get(GeneratedValue.class).getAnnotation()).strategy()
.equals(InternalIdStrategy.class);
return hasIdAnnotation && hasStrategyOtherThanInternal;
}
public IdStrategy idStrategy() {
// Forces initialization of the id strategy
return getOrComputePrimaryIndexField()
.map(ignored -> idStrategy).orElse(null);
}
public Class extends IdStrategy> idStrategyClass() {
return idStrategyClass;
}
public void registerIdGenerationStrategy(IdStrategy strategy) {
if (strategy.getClass().equals(idStrategyClass)) {
idStrategy = strategy;
} else {
throw new IllegalArgumentException("Strategy " + strategy +
" is not an instance of " + idStrategyClass);
}
}
public MethodInfo postLoadMethodOrNull() {
Optional result = this.postLoadMethod;
if (result == null) {
synchronized (this) {
result = this.postLoadMethod;
if (result == null) {
Collection possiblePostLoadMethods = methodsInfo
.findMethodInfoBy(methodInfo -> methodInfo.hasAnnotation(PostLoad.class));
if (possiblePostLoadMethods.size() > 1) {
throw new MetadataException(String
.format("Cannot have more than one post load method annotated with @PostLoad for class '%s'",
this.className));
}
this.postLoadMethod = possiblePostLoadMethods.stream().findFirst();
result = this.postLoadMethod;
}
}
}
return result.orElse(null);
}
public FieldInfo getFieldInfo(String propertyName) {
// fall back to the field if method cannot be found
FieldInfo optionalLabelField = labelFieldOrNull();
if (optionalLabelField != null && optionalLabelField.getName().equals(propertyName)) {
return optionalLabelField;
}
FieldInfo propertyField = propertyField(propertyName);
if (propertyField != null) {
return propertyField;
}
return fieldsInfo.get(propertyName);
}
/**
* Return a FieldInfo for the EndNode of a RelationshipEntity
*
* @return a FieldInfo for the field annotated as the EndNode, or none if not found
*/
public FieldInfo getEndNodeReader() {
Optional result = this.endNodeReader;
if (result == null) {
synchronized (this) {
result = this.endNodeReader;
if (result == null) {
if (isRelationshipEntity()) {
endNodeReader = fieldsInfo().fields().stream()
.filter(fieldInfo -> fieldInfo.getAnnotations().get(EndNode.class) != null)
.findFirst();
if (!endNodeReader.isPresent()) {
LOGGER.warn("Failed to find an @EndNode on {}", name());
}
} else {
endNodeReader = Optional.empty();
}
result = this.endNodeReader;
}
}
}
return result.orElse(null);
}
/**
* Return a FieldInfo for the StartNode of a RelationshipEntity
*
* @return a FieldInfo for the field annotated as the StartNode, or none if not found
*/
public FieldInfo getStartNodeReader() {
Optional result = this.startNodeReader;
if (result == null) {
synchronized (this) {
result = this.startNodeReader;
if (result == null) {
if (isRelationshipEntity()) {
startNodeReader = fieldsInfo().fields().stream()
.filter(fieldInfo -> fieldInfo.getAnnotations().get(StartNode.class) != null)
.findFirst();
if (!startNodeReader.isPresent()) {
LOGGER.warn("Failed to find an @StartNode on {}", name());
}
} else {
startNodeReader = Optional.empty();
}
result = this.startNodeReader;
}
}
}
return result.orElse(null);
}
/**
* Returns if the class as fields annotated with @Required annotation
*/
public boolean hasRequiredFields() {
return !requiredFields().isEmpty();
}
public Collection requiredFields() {
if (requiredFields == null) {
requiredFields = new ArrayList<>();
for (FieldInfo fieldInfo : propertyFields()) {
if (fieldInfo.getAnnotations().has(Required.class)) {
requiredFields.add(fieldInfo);
}
}
}
return requiredFields;
}
public boolean hasVersionField() {
return getOrComputeVersionField().isPresent();
}
public FieldInfo getVersionField() {
return getOrComputeVersionField().orElse(null);
}
private Optional getOrComputeVersionField() {
Optional result = this.versionField;
if (result == null) {
synchronized (this) {
result = this.versionField;
if (result == null) {
Collection fields = getFieldInfos(FieldInfo::isVersionField);
if (fields.size() > 1) {
throw new MetadataException("Only one version field is allowed, found " + fields);
}
Iterator iterator = fields.iterator();
if (iterator.hasNext()) {
this.versionField = Optional.of(iterator.next());
} else {
// cache that there is no version field
this.versionField = Optional.empty();
}
result = this.versionField;
}
}
}
return result;
}
/**
* Reads the value of the entity's primary index field if any.
*
* @param entity
* @return
*/
public Object readPrimaryIndexValueOf(Object entity) {
Objects.requireNonNull(entity, "Entity to read from must not be null.");
Object value = null;
if (this.hasPrimaryIndexField()) {
// One has to use #read here to get the ID as defined in the entity.
// #readProperty gives back the converted value the database sees.
// This breaks immediate in LoadOneDelegate#lookup(Class, Object).
// That is called by LoadOneDelegate#load(Class, Serializable, int)
// immediately after loading (and finding(!!) an entity, which is never
// returned directly but goes through a cache.
// However, LoadOneDelegate#load(Class, Serializable, int) deals with the
// ID as defined in the domain and so we have to use that in the same way here.
value = this.primaryIndexField().read(entity);
}
return value;
}
public Function