Java tutorial
/* * Copyright (c) 2002-2016 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. * * This product may include a number of subcomponents with * separate copyright notices and license terms. Your use of the source * code for these subcomponents is subject to the terms and * conditions of the subcomponent's license, as noted in the LICENSE file. */ package org.neo4j.ogm.metadata; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.collections4.CollectionUtils; import org.neo4j.ogm.utils.ClassUtils; import org.neo4j.ogm.annotation.GraphId; import org.neo4j.ogm.annotation.NodeEntity; import org.neo4j.ogm.annotation.Property; import org.neo4j.ogm.annotation.Relationship; import org.neo4j.ogm.annotation.RelationshipEntity; import org.neo4j.ogm.annotation.Transient; import org.neo4j.ogm.annotation.Labels; import org.neo4j.ogm.classloader.MetaDataClassLoader; import org.neo4j.ogm.exception.MappingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Maintains object to graph mapping details at the class (type) level * <p> * 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. * <p> * 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. * <p> * 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 */ public class ClassInfo { private static final Logger LOGGER = LoggerFactory.getLogger(ClassInfo.class); private final List<ClassInfo> directSubclasses = new ArrayList<>(); private final List<ClassInfo> directInterfaces = new ArrayList<>(); private final List<ClassInfo> directImplementingClasses = new ArrayList<>(); /** * ISSUE-180: synchronized can be used instead of this lock but right now this mechanism is here to see if * ConcurrentModificationException stops occurring. */ private final Lock lock = new ReentrantLock(); private String className; private String directSuperclassName; private String neo4jName; private boolean isInterface; private boolean isAbstract; private boolean isEnum; private boolean hydrated; private FieldsInfo fieldsInfo = new FieldsInfo(); private MethodsInfo methodsInfo = new MethodsInfo(); private AnnotationsInfo annotationsInfo = new AnnotationsInfo(); private InterfacesInfo interfacesInfo = new InterfacesInfo(); private ClassInfo directSuperclass; private Map<Class, List<MethodInfo>> iterableGettersForType = new HashMap<>(); private Map<Class, List<MethodInfo>> iterableSettersForType = new HashMap<>(); private Map<Class, List<FieldInfo>> iterableFieldsForType = new HashMap<>(); private Map<FieldInfo, Field> fieldInfoFields = new ConcurrentHashMap<>(); private volatile Set<FieldInfo> fieldInfos; private volatile Map<String, FieldInfo> propertyFields; private volatile Map<String, FieldInfo> indexFields; private volatile FieldInfo identityField = null; private volatile FieldInfo labelField = null; private volatile boolean labelFieldMapped = false; // todo move this to a factory class public ClassInfo(InputStream inputStream) throws IOException { DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, 1024)); // Magic if (dataInputStream.readInt() != 0xCAFEBABE) { return; } dataInputStream.readUnsignedShort(); //minor version dataInputStream.readUnsignedShort(); // major version ConstantPool constantPool = new ConstantPool(dataInputStream); // Access flags int flags = dataInputStream.readUnsignedShort(); isInterface = (flags & 0x0200) != 0; isAbstract = (flags & 0x0400) != 0; isEnum = (flags & 0x4000) != 0; className = constantPool.lookup(dataInputStream.readUnsignedShort()).replace('/', '.'); String sce = constantPool.lookup(dataInputStream.readUnsignedShort()); if (sce != null) { directSuperclassName = sce.replace('/', '.'); } interfacesInfo = new InterfacesInfo(dataInputStream, constantPool); fieldsInfo = new FieldsInfo(dataInputStream, constantPool); methodsInfo = new MethodsInfo(dataInputStream, constantPool); annotationsInfo = new AnnotationsInfo(dataInputStream, constantPool); new ClassValidator(this).validate(); } /** * This class was referenced as a superclass of the given subclass. * * @param name the name of the class * @param subclass {@link ClassInfo} of the subclass */ public ClassInfo(String name, ClassInfo subclass) { this.className = name; this.hydrated = false; addSubclass(subclass); } /** * A class that was previously only seen as a temp superclass of another class can now be fully hydrated. * * @param classInfoDetails ClassInfo details */ public void hydrate(ClassInfo classInfoDetails) { if (!this.hydrated) { this.hydrated = true; this.isAbstract = classInfoDetails.isAbstract; this.isInterface = classInfoDetails.isInterface; this.isEnum = classInfoDetails.isEnum; this.directSuperclassName = classInfoDetails.directSuperclassName; //this.interfaces.addAll(classInfoDetails.interfaces()); this.interfacesInfo.append(classInfoDetails.interfacesInfo()); this.annotationsInfo.append(classInfoDetails.annotationsInfo()); this.fieldsInfo.append(classInfoDetails.fieldsInfo()); this.methodsInfo.append(classInfoDetails.methodsInfo()); } } void extend(ClassInfo classInfo) { //this.interfaces.addAll(classInfo.interfaces()); 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 */ public 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 boolean hydrated() { return hydrated; } public String name() { return className; } String simpleName() { return className.substring(className.lastIndexOf('.') + 1); } ClassInfo directSuperclass() { return directSuperclass; } /** * <p> * 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. * </p> * <p> * Note that this method returns only the static labels. A node entity instance may declare additional labels * manged 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. * </p> * * @return A {@link Collection} of all the static labels that apply to the node or an empty list if there aren't * any, never <code>null</code> */ public Collection<String> staticLabels() { return collectLabels(new ArrayList<String>()); } public String neo4jName() { if (neo4jName == null) { try { lock.lock(); 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; } neo4jName = simpleName(); } } finally { lock.unlock(); } } return neo4jName; } private Collection<String> collectLabels(Collection<String> labelNames) { if (!isAbstract || annotationsInfo.get(NodeEntity.CLASS) != null) { labelNames.add(neo4jName()); } if (directSuperclass != null && !"java.lang.Object".equals(directSuperclass.className)) { directSuperclass.collectLabels(labelNames); } for (ClassInfo interfaceInfo : directInterfaces()) { interfaceInfo.collectLabels(labelNames); } return labelNames; } public List<ClassInfo> directSubclasses() { return directSubclasses; } public List<ClassInfo> directImplementingClasses() { return directImplementingClasses; } public List<ClassInfo> directInterfaces() { return directInterfaces; } public org.neo4j.ogm.metadata.InterfacesInfo interfacesInfo() { return interfacesInfo; } public Collection<AnnotationInfo> annotations() { return annotationsInfo.list(); } public boolean isInterface() { return isInterface; } public boolean isEnum() { return isEnum; } public org.neo4j.ogm.metadata.AnnotationsInfo annotationsInfo() { return annotationsInfo; } public String superclassName() { return directSuperclassName; } public org.neo4j.ogm.metadata.FieldsInfo fieldsInfo() { return fieldsInfo; } public org.neo4j.ogm.metadata.MethodsInfo methodsInfo() { return methodsInfo; } @Override public String toString() { return name(); } private FieldInfo identityFieldOrNull() { try { return identityField(); } catch (MappingException me) { return 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 <code>null</code> * @throws MappingException if no identity field can be found */ public FieldInfo identityField() { if (identityField != null) { return identityField; } try { lock.lock(); if (identityField == null) { for (FieldInfo fieldInfo : fieldsInfo().fields()) { AnnotationInfo annotationInfo = fieldInfo.getAnnotations().get(GraphId.CLASS); if (annotationInfo != null) { if (fieldInfo.getDescriptor().equals("Ljava/lang/Long;")) { identityField = fieldInfo; return fieldInfo; } } } FieldInfo fieldInfo = fieldsInfo().get("id"); if (fieldInfo != null) { if (fieldInfo.getDescriptor().equals("Ljava/lang/Long;")) { identityField = fieldInfo; return fieldInfo; } } throw new MappingException("No identity field found for class: " + this.className); } else { return identityField; } } finally { lock.unlock(); } } /** * The label field is an optional field annotated with @Labels. * * @return A {@link FieldInfo} object representing the label field. Optionally <code>null</code> */ public FieldInfo labelFieldOrNull() { if (labelFieldMapped) { return labelField; } try { lock.lock(); 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; } } } else { return labelField; } } finally { lock.unlock(); } return null; } public boolean isRelationshipEntity() { for (AnnotationInfo info : annotations()) { if (info.getName().equals(RelationshipEntity.CLASS)) { 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 */ public Collection<FieldInfo> propertyFields() { if (fieldInfos == null) { try { lock.lock(); if (fieldInfos == null) { FieldInfo identityField = identityFieldOrNull(); fieldInfos = new HashSet<>(); for (FieldInfo fieldInfo : fieldsInfo().fields()) { if (fieldInfo != identityField && !fieldInfo.isLabelField()) { AnnotationInfo annotationInfo = fieldInfo.getAnnotations().get(Property.CLASS); if (annotationInfo == null) { if (fieldInfo.persistableAsProperty()) { fieldInfos.add(fieldInfo); } } else { fieldInfos.add(fieldInfo); } } } } } finally { lock.unlock(); } } return fieldInfos; } /** * 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. */ public FieldInfo propertyField(String propertyName) { if (propertyFields == null) { try { lock.lock(); if (propertyFields == null) { Collection<FieldInfo> fieldInfos = propertyFields(); propertyFields = new HashMap<>(fieldInfos.size()); for (FieldInfo fieldInfo : fieldInfos) { propertyFields.put(fieldInfo.property().toLowerCase(), fieldInfo); } } } finally { lock.unlock(); } } return propertyFields.get(propertyName.toLowerCase()); } /** * 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<FieldInfo> relationshipFields() { FieldInfo identityField = identityFieldOrNull(); Set<FieldInfo> fieldInfos = new HashSet<>(); for (FieldInfo fieldInfo : fieldsInfo().fields()) { if (fieldInfo != identityField) { AnnotationInfo annotationInfo = fieldInfo.getAnnotations().get(Relationship.CLASS); if (annotationInfo == null) { if (!fieldInfo.persistableAsProperty()) { fieldInfos.add(fieldInfo); } } else { fieldInfos.add(fieldInfo); } } } return fieldInfos; } /** * 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 * @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, String relationshipDirection, boolean strict) { for (FieldInfo fieldInfo : relationshipFields()) { String relationship = strict ? fieldInfo.relationshipTypeAnnotation() : fieldInfo.relationship(); if (relationshipName.equalsIgnoreCase(relationship)) { if (((fieldInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || fieldInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && (relationshipDirection.equals(Relationship.INCOMING))) || (relationshipDirection.equals(Relationship.OUTGOING) && !(fieldInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { 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<FieldInfo> candidateRelationshipFields(String relationshipName, String relationshipDirection, boolean strict) { Set<FieldInfo> candidateFields = new HashSet<>(); for (FieldInfo fieldInfo : relationshipFields()) { String relationship = strict ? fieldInfo.relationshipTypeAnnotation() : fieldInfo.relationship(); if (relationshipName.equalsIgnoreCase(relationship)) { if (((fieldInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || fieldInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && (relationshipDirection.equals(Relationship.INCOMING))) || (relationshipDirection.equals(Relationship.OUTGOING) && !(fieldInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { 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; } /** * The identity getter is any getter annotated with @NodeId returning a Long, or if none exists, a getter * returning Long called 'getId' * * @return A FieldInfo object representing the identity field or null if it doesn't exist */ public MethodInfo identityGetter() { for (MethodInfo methodInfo : methodsInfo().getters()) { AnnotationInfo annotationInfo = methodInfo.getAnnotations().get(GraphId.CLASS); if (annotationInfo != null) { if (methodInfo.getDescriptor().equals("()Ljava/lang/Long;")) { return methodInfo; } } } MethodInfo methodInfo = methodsInfo().get("getId"); if (methodInfo != null) { if (methodInfo.getDescriptor().equals("()Ljava/lang/Long;")) { return methodInfo; } } return null; } /** * The identity setter is any setter annotated with @NodeId taking a Long parameter, or if none exists, a setter * called 'setId' taking a Long parameter * * @return A FieldInfo object representing the identity field or null if it doesn't exist */ public MethodInfo identitySetter() { for (MethodInfo methodInfo : methodsInfo().setters()) { AnnotationInfo annotationInfo = methodInfo.getAnnotations().get(GraphId.CLASS); if (annotationInfo != null) { if (methodInfo.getDescriptor().equals("(Ljava/lang/Long;)V")) { return methodInfo; } } } MethodInfo methodInfo = methodsInfo().get("setId"); if (methodInfo != null) { if (methodInfo.getDescriptor().equals("(Ljava/lang/Long;)V")) { return methodInfo; } } return null; } /** * A property getter is any getter annotated with @Property, or any getter whose return type can be mapped to a * node property. The identity getter is not a property getter. * * @return A Collection of MethodInfo objects describing the classInfo's property getters */ public Collection<MethodInfo> propertyGetters() { MethodInfo identityGetter = identityGetter(); Set<MethodInfo> propertyGetters = new HashSet<>(); for (MethodInfo methodInfo : methodsInfo().getters()) { if (!methodInfo.isEquallyNamed(identityGetter)) { AnnotationInfo annotationInfo = methodInfo.getAnnotations().get(Property.CLASS); if (annotationInfo == null) { if (methodInfo.isSimpleGetter()) { propertyGetters.add(methodInfo); } } else { propertyGetters.add(methodInfo); } } } return propertyGetters; } /** * A property setter is any setter annotated with @Property, or any setter whose parameter type can be mapped to a * node property. The identity setter is not a property setter. * * @return A Collection of MethodInfo objects describing the classInfo's property setters */ public Collection<MethodInfo> propertySetters() { MethodInfo identitySetter = identitySetter(); Set<MethodInfo> propertySetters = new HashSet<>(); for (MethodInfo methodInfo : methodsInfo().setters()) { if (!methodInfo.isEquallyNamed(identitySetter)) { AnnotationInfo annotationInfo = methodInfo.getAnnotations().get(Property.CLASS); if (annotationInfo == null) { if (methodInfo.isSimpleSetter()) { propertySetters.add(methodInfo); } } else { propertySetters.add(methodInfo); } } } return propertySetters; } public Collection<MethodInfo> propertyGettersAndSetters() { return CollectionUtils.union(propertyGetters(), propertySetters()); } /** * A relationship getter is any getter annotated with @Relationship, or any getter whose return type cannot be mapped to a * node property. The identity getter is not a property getter. * * @return A Collection of MethodInfo objects describing the classInfo's property getters */ public Collection<MethodInfo> relationshipGetters() { MethodInfo identityGetter = identityGetter(); Set<MethodInfo> relationshipGetters = new HashSet<>(); for (MethodInfo methodInfo : methodsInfo().getters()) { if (identityGetter == null || !methodInfo.getName().equals(identityGetter.getName())) { AnnotationInfo annotationInfo = methodInfo.getAnnotations().get(Relationship.CLASS); if (annotationInfo == null) { if (!methodInfo.isSimpleGetter()) { relationshipGetters.add(methodInfo); } } else { relationshipGetters.add(methodInfo); } } } return relationshipGetters; } /** * A relationship setter is any setter annotated with @Relationship, or any setter whose parameter type cannot be mapped to a * node property. The identity setter is not a property getter. * * @return A Collection of MethodInfo objects describing the classInfo's property getters */ public Collection<MethodInfo> relationshipSetters() { MethodInfo identitySetter = identitySetter(); Set<MethodInfo> relationshipSetters = new HashSet<>(); for (MethodInfo methodInfo : methodsInfo().setters()) { if (identitySetter == null || !methodInfo.getName().equals(identitySetter.getName())) { AnnotationInfo annotationInfo = methodInfo.getAnnotations().get(Relationship.CLASS); if (annotationInfo == null) { if (!methodInfo.isSimpleSetter()) { relationshipSetters.add(methodInfo); } } else { relationshipSetters.add(methodInfo); } } } return relationshipSetters; } /** * Finds the relationship getter with a specific name from the specified ClassInfo's relationship getters * * @param relationshipName the relationshipName of the getter to find * @return A MethodInfo object describing the required relationship getter, or null if it doesn't exist. */ public MethodInfo relationshipGetter(String relationshipName) { for (MethodInfo methodInfo : relationshipGetters()) { if (methodInfo.relationship().equalsIgnoreCase(relationshipName)) { return methodInfo; } } return null; } /** * Finds the relationship getter with a specific name and direction from the specified ClassInfo's relationship getters * * @param relationshipName the relationshipName of the getter to find * @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 MethodInfo * @return A MethodInfo object describing the required relationship getter, or null if it doesn't exist. */ public MethodInfo relationshipGetter(String relationshipName, String relationshipDirection, boolean strict) { for (MethodInfo methodInfo : relationshipGetters()) { String relationship = strict ? methodInfo.relationshipTypeAnnotation() : methodInfo.relationship(); if (relationshipName.equalsIgnoreCase(relationship)) { if (((methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && relationshipDirection.equals(Relationship.INCOMING)) || (relationshipDirection.equals(Relationship.OUTGOING) && !(methodInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { return methodInfo; } } } return null; } /** * Finds the relationship setter with a specific name from the specified ClassInfo's relationship setters * * @param relationshipName the relationshipName of the setter to find * @return A MethodInfo object describing the required relationship setter, or null if it doesn't exist. */ public MethodInfo relationshipSetter(String relationshipName) { for (MethodInfo methodInfo : relationshipSetters()) { if (methodInfo.relationship().equalsIgnoreCase(relationshipName)) { return methodInfo; } } return null; } /** * Finds the relationship setter with a specific name and direction from the specified ClassInfo's relationship setters. * * @param relationshipName the relationshipName of the setter to find * @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 MethodInfo * @return A MethodInfo object describing the required relationship setter, or null if it doesn't exist. */ public MethodInfo relationshipSetter(String relationshipName, String relationshipDirection, boolean strict) { for (MethodInfo methodInfo : relationshipSetters()) { String relationship = strict ? methodInfo.relationshipTypeAnnotation() : methodInfo.relationship(); if (relationshipName.equalsIgnoreCase(relationship)) { if (((methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && relationshipDirection.equals(Relationship.INCOMING)) || (relationshipDirection.equals(Relationship.OUTGOING) && !(methodInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { return methodInfo; } } } return null; } /** * Finds all relationship setters with a specific name and direction from the specified ClassInfo's relationship setters. * * @param relationshipName the relationshipName of the setter to find * @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 MethodInfo * @return A Set of MethodInfo object describing the required relationship setter, or empty set if it doesn't exist. */ public Set<MethodInfo> candidateRelationshipSetters(String relationshipName, String relationshipDirection, boolean strict) { Set<MethodInfo> candidateSetters = new HashSet<>(); for (MethodInfo methodInfo : relationshipSetters()) { String relationship = strict ? methodInfo.relationshipTypeAnnotation() : methodInfo.relationship(); if (relationshipName.equalsIgnoreCase(relationship)) { if (((methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && relationshipDirection.equals(Relationship.INCOMING)) || (relationshipDirection.equals(Relationship.OUTGOING) && !(methodInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { candidateSetters.add(methodInfo); } } } return candidateSetters; } /** * Finds the property setter with a specific name from the specified ClassInfo's property setters * * @param propertyName the propertyName of the setter to find * @return A MethodInfo object describing the required property setter, or null if it doesn't exist. */ public MethodInfo propertySetter(String propertyName) { for (MethodInfo methodInfo : propertySetters()) { String match = methodInfo.property(); if (match.equalsIgnoreCase(propertyName) || match.equalsIgnoreCase("set" + propertyName)) { return methodInfo; } } return null; } /** * Finds the property getter with a specific name from the specified ClassInfo's property getters * * @param propertyName the propertyName of the getter to find * @return A MethodInfo object describing the required property getter, or null if it doesn't exist. */ public MethodInfo propertyGetter(String propertyName) { for (MethodInfo methodInfo : propertyGetters()) { String match = methodInfo.property(); if (match.equalsIgnoreCase(propertyName) || match.equalsIgnoreCase("get" + propertyName)) { return methodInfo; } } return null; } public Field getField(FieldInfo fieldInfo) { Field field = fieldInfoFields.get(fieldInfo); if (field != null) { return field; } try { field = MetaDataClassLoader.loadClass(name()).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"); } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } @SuppressWarnings("unchecked") public Method getMethod(MethodInfo methodInfo, Class... parameterTypes) { try { return MetaDataClassLoader.loadClass(name()).getMethod(methodInfo.getName(), parameterTypes); } catch (Exception e) { throw new RuntimeException(e); } } /** * Find all setter MethodInfos for the specified ClassInfo whose parameter type matches the supplied class * * @param parameterType The setter parameter type to look for. * @return A {@link List} of {@link MethodInfo} objects that accept the given parameter type, never <code>null</code> */ public List<MethodInfo> findSetters(Class<?> parameterType) { String setterSignature = "(L" + parameterType.getName().replace(".", "/") + ";)V"; List<MethodInfo> methodInfos = new ArrayList<>(); for (MethodInfo methodInfo : methodsInfo().methods()) { if (methodInfo.getDescriptor().equals(setterSignature)) { methodInfos.add(methodInfo); } } return methodInfos; } /** * Find all getter MethodInfos for the specified ClassInfo whose return type matches the supplied class * * @param returnType The getter return type to look for. * @return A {@link List} of {@link MethodInfo} objects that return the given type, never <code>null</code> */ public List<MethodInfo> findGetters(Class<?> returnType) { String setterSignature = "()L" + returnType.getName().replace(".", "/") + ";"; List<MethodInfo> methodInfos = new ArrayList<>(); for (MethodInfo methodInfo : methodsInfo().methods()) { if (methodInfo.getDescriptor().equals(setterSignature)) { methodInfos.add(methodInfo); } } return methodInfos; } /** * 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 <code>null</code> */ public List<FieldInfo> findFields(Class<?> fieldType) { String fieldSignature = "L" + fieldType.getName().replace(".", "/") + ";"; List<FieldInfo> fieldInfos = new ArrayList<>(); for (FieldInfo fieldInfo : fieldsInfo().fields()) { if (fieldInfo.getDescriptor().equals(fieldSignature)) { fieldInfos.add(fieldInfo); } } return fieldInfos; } /** * 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 <code>null</code> */ public List<FieldInfo> findFields(String annotation) { List<FieldInfo> fieldInfos = new ArrayList<>(); for (FieldInfo fieldInfo : fieldsInfo().fields()) { if (fieldInfo.hasAnnotation(annotation)) { fieldInfos.add(fieldInfo); } } return fieldInfos; } /** * 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<FieldInfo> findIterableFields() { List<FieldInfo> fieldInfos = new ArrayList<>(); try { for (FieldInfo fieldInfo : fieldsInfo().fields()) { Class type = getField(fieldInfo).getType(); if (type.isArray() || Iterable.class.isAssignableFrom(type)) { fieldInfos.add(fieldInfo); } } return fieldInfos; } 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 <code>null</code> */ public List<FieldInfo> findIterableFields(Class iteratedType) { if (iterableFieldsForType.containsKey(iteratedType)) { return iterableFieldsForType.get(iteratedType); } List<FieldInfo> fieldInfos = new ArrayList<>(); String typeSignature = "L" + iteratedType.getName().replace('.', '/') + ";"; String arrayOfTypeSignature = "[" + typeSignature; try { for (FieldInfo fieldInfo : fieldsInfo().fields()) { if (fieldInfo.getTypeParameterDescriptor() != null) { if (fieldInfo.getTypeParameterDescriptor().equals(typeSignature) || fieldInfo.isParameterisedTypeOf(iteratedType)) { fieldInfos.add(fieldInfo); } } else if (fieldInfo.getDescriptor().equals(arrayOfTypeSignature) || fieldInfo.isParameterisedTypeOf(iteratedType)) { fieldInfos.add(fieldInfo); } } iterableFieldsForType.put(iteratedType, fieldInfos); return fieldInfos; } 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 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 <code>null</code> */ public List<FieldInfo> findIterableFields(Class iteratedType, String relationshipType, String relationshipDirection, boolean strict) { List<FieldInfo> fieldInfos = new ArrayList<>(); for (FieldInfo fieldInfo : findIterableFields(iteratedType)) { String relationship = strict ? fieldInfo.relationshipTypeAnnotation() : fieldInfo.relationship(); if (relationshipType.equals(relationship)) { if (((fieldInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || fieldInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && relationshipDirection.equals(Relationship.INCOMING)) || (relationshipDirection.equals(Relationship.OUTGOING) && !(fieldInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { fieldInfos.add(fieldInfo); } } } return fieldInfos; } /** * Finds all setter methods whose parameter signature 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 <code>null</code> */ public List<MethodInfo> findIterableSetters(Class iteratedType) { if (iterableSettersForType.containsKey(iteratedType)) { return iterableSettersForType.get(iteratedType); } List<MethodInfo> methodInfos = new ArrayList<>(); String typeSignature = "L" + iteratedType.getName().replace('.', '/') + ";"; String arrayOfTypeSignature = "([" + typeSignature + ")V"; try { for (MethodInfo methodInfo : propertySetters()) { if (methodInfo.getTypeParameterDescriptor() != null) { if (methodInfo.getTypeParameterDescriptor().equals(typeSignature) || methodInfo.isParameterisedTypeOf(iteratedType)) { methodInfos.add(methodInfo); } } else { if (methodInfo.getDescriptor().equals(arrayOfTypeSignature) || methodInfo.isParameterisedTypeOf(iteratedType)) { methodInfos.add(methodInfo); } } } for (MethodInfo methodInfo : relationshipSetters()) { if (methodInfo.getTypeParameterDescriptor() != null) { if (methodInfo.getTypeParameterDescriptor().equals(typeSignature) || methodInfo.isParameterisedTypeOf(iteratedType)) { methodInfos.add(methodInfo); } else { if (methodInfo.getDescriptor().equals(arrayOfTypeSignature) || methodInfo.isParameterisedTypeOf(iteratedType)) { methodInfos.add(methodInfo); } } } } iterableSettersForType.put(iteratedType, methodInfos); return methodInfos; } catch (Exception e) { throw new RuntimeException(e); } } /** * Finds all setter methods whose parameter signature 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 this setter is annotated with is "relationshipType" * and the relationship direction matches "relationshipDirection" * * @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 MethodInfo * @return {@link List} of {@link MethodInfo}, never <code>null</code> */ public List<MethodInfo> findIterableSetters(Class iteratedType, String relationshipType, String relationshipDirection, boolean strict) { List<MethodInfo> methodInfos = new ArrayList<>(); for (MethodInfo methodInfo : findIterableSetters(iteratedType)) { String relationship = strict ? methodInfo.relationshipTypeAnnotation() : methodInfo.relationship(); if (relationshipType.equals(relationship)) { if (((methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && relationshipDirection.equals(Relationship.INCOMING)) || (relationshipDirection.equals(Relationship.OUTGOING) && !(methodInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { methodInfos.add(methodInfo); } } } return methodInfos; } /** * Finds all getter methods whose parameterised return 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 <code>null</code> */ public List<MethodInfo> findIterableGetters(Class iteratedType) { if (iterableGettersForType.containsKey(iteratedType)) { return iterableGettersForType.get(iteratedType); } List<MethodInfo> methodInfos = new ArrayList<>(); String typeSignature = "L" + iteratedType.getName().replace('.', '/') + ";"; String arrayOfTypeSignature = "()[" + typeSignature; try { for (MethodInfo methodInfo : propertyGetters()) { if (methodInfo.getTypeParameterDescriptor() != null) { if (methodInfo.getTypeParameterDescriptor().equals(typeSignature)) { methodInfos.add(methodInfo); } } else { if (methodInfo.getDescriptor().equals(arrayOfTypeSignature)) { methodInfos.add(methodInfo); } } } for (MethodInfo methodInfo : relationshipGetters()) { if (methodInfo.getTypeParameterDescriptor() != null) { if (methodInfo.getTypeParameterDescriptor().equals(typeSignature)) { methodInfos.add(methodInfo); } else { if (methodInfo.getDescriptor().equals(arrayOfTypeSignature)) { methodInfos.add(methodInfo); } } } } iterableGettersForType.put(iteratedType, methodInfos); return methodInfos; } catch (Exception e) { throw new RuntimeException(e); } } /** * Finds all getter methods whose parameterised return 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 this getter is annotated with is "relationshipType" * and the direction of the relationship is "relationshipDirection" * * @param iteratedType the type of iterable * @param relationshipType the relationship type * @param relationshipDirection the relationshipDirection * @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 MethodInfo * @return {@link List} of {@link MethodInfo}, never <code>null</code> */ public List<MethodInfo> findIterableGetters(Class iteratedType, String relationshipType, String relationshipDirection, boolean strict) { List<MethodInfo> methodInfos = new ArrayList<>(); for (MethodInfo methodInfo : findIterableGetters(iteratedType)) { String relationship = strict ? methodInfo.relationshipTypeAnnotation() : methodInfo.relationship(); if (relationshipType.equals(relationship)) { if (((methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING) || methodInfo.relationshipDirection(Relationship.OUTGOING).equals(Relationship.UNDIRECTED)) && relationshipDirection.equals(Relationship.INCOMING)) || (relationshipDirection.equals(Relationship.OUTGOING) && !(methodInfo .relationshipDirection(Relationship.OUTGOING).equals(Relationship.INCOMING)))) { methodInfos.add(methodInfo); } } } return methodInfos; } 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 */ public boolean isSubclassOf(ClassInfo classInfo) { if (this == classInfo) { return true; } boolean found = false; for (ClassInfo subclass : classInfo.directSubclasses()) { found = isSubclassOf(subclass); if (found) { break; } } return found; } public Class<?> getType(String typeParameterDescriptor) { return ClassUtils.getType(typeParameterDescriptor); } /** * Get the underlying class represented by this ClassInfo * * @return the underlying class or null if it cannot be determined */ public Class getUnderlyingClass() { try { return MetaDataClassLoader.loadClass(className);//Class.forName(className); } catch (ClassNotFoundException e) { LOGGER.error("Could not get underlying class for {}", className); } return null; } /** * Gets the class of the type parameter description of the entity related to this. * The match is done based on the following- * 1. Look for a setter explicitly annotated with @Relationship for a type and implied direction * 2. Look for a field explicitly annotated with @Relationship for a type and implied direction * 3. Look for a setter with name derived from the relationship type for the given 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 */ public Class getTypeParameterDescriptorForRelationship(String relationshipType, String 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 { MethodInfo methodInfo = relationshipSetter(relationshipType, relationshipDirection, STRICT_MODE); if (methodInfo != null && methodInfo.getTypeDescriptor() != null) { return ClassUtils.getType(methodInfo.getTypeDescriptor()); } FieldInfo fieldInfo = relationshipField(relationshipType, relationshipDirection, STRICT_MODE); if (fieldInfo != null && fieldInfo.getTypeDescriptor() != null) { return ClassUtils.getType(fieldInfo.getTypeDescriptor()); } if (!relationshipDirection.equals(Relationship.INCOMING)) { //we always expect an annotation for INCOMING methodInfo = relationshipSetter(relationshipType, relationshipDirection, INFERRED_MODE); if (methodInfo != null && methodInfo.getTypeDescriptor() != null) { return ClassUtils.getType(methodInfo.getTypeDescriptor()); } fieldInfo = relationshipField(relationshipType, relationshipDirection, INFERRED_MODE); if (fieldInfo != null && fieldInfo.getTypeDescriptor() != null) { return ClassUtils.getType(fieldInfo.getTypeDescriptor()); } } } catch (RuntimeException e) { LOGGER.debug("Could not get {} class type for relationshipType {} and relationshipDirection {} ", className, relationshipType, relationshipDirection); } return null; } }