package sk.nociar.jpacloner; import static java.util.Collections.unmodifiableList; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.ManyToMany; import javax.persistence.OneToMany; import sk.nociar.jpacloner.properties.FieldPropertyReader; import sk.nociar.jpacloner.properties.FieldPropertyWriter; import sk.nociar.jpacloner.properties.MethodPropertyReader; import sk.nociar.jpacloner.properties.MethodPropertyWriter; import sk.nociar.jpacloner.properties.PropertyReader; import sk.nociar.jpacloner.properties.PropertyWriter; /** * Info about JPA class. The class info also considers the {@link AccessType} of an {@link Entity} or {@link Embeddable} class. * For more information about the {@link AccessType} handling, please see * <a href="http://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html_single/#d5e3119">Hibernate documentation</a>. * * @author Miroslav Nociar */ public class JpaClassInfo { private final Constructor<?> constructor; private final Map<String, Field> fields = new HashMap<String, Field>(); private final Map<String, Method> getters = new HashMap<String, Method>(); private final Map<String, Method> setters = new HashMap<String, Method>(); /** Holds all JPA properties (basic and relations) */ private final Map<String, JpaPropertyInfo> jpaProperties = new HashMap<String, JpaPropertyInfo>(); private final List<String> baseProperties; private final List<String> relations; private static final ConcurrentMap<Class<?>, JpaClassInfo> classInfo = new ConcurrentHashMap<Class<?>, JpaClassInfo>(); public static JpaClassInfo get(Class<?> clazz) { clazz = getJpaClass(clazz); if (clazz == null) { return null; } JpaClassInfo info = classInfo.get(clazz); if (info == null) { // create information for the class info = new JpaClassInfo(clazz); classInfo.putIfAbsent(clazz, info); } return info; } /** * Returns the raw JPA class (i.e. annotated by {@link Entity} or {@link Embeddable}) or <code>null</code>. */ public static Class<?> getJpaClass(Class<?> c) { while (c != null) { if (c.getAnnotation(Entity.class) != null || c.getAnnotation(Embeddable.class) != null) { return c; } c = c.getSuperclass(); } return null; } private JpaClassInfo(final Class<?> clazz) { // find default constructor try { constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); } catch (NoSuchMethodException e) { throw new IllegalStateException("Unable to find default constructor for class: " + clazz, e); } // scan for all fields, getters and setters process(clazz); // determine the default access type AccessType accessType = null; for (Class<?> c = clazz; accessType == null && c != null; c = c.getSuperclass()) { Access access = clazz.getAnnotation(Access.class); if (access != null) { accessType = access.value(); } } if (accessType == null) { // try to find @Id or @EmbeddedId in fields for (Field f : fields.values()) { if (f.getAnnotation(Id.class) != null || f.getAnnotation(EmbeddedId.class) != null) { accessType = AccessType.FIELD; break; } } } if (accessType == null) { // use the PROPERTY access type accessType = AccessType.PROPERTY; } // scan all getters for (String propertyName : getters.keySet()) { Method getter = getters.get(propertyName); Method setter = setters.get(propertyName); // determine access type AccessType ac = accessType; Access access = getter.getAnnotation(Access.class); if (access != null) { ac = access.value(); } if (ac == AccessType.PROPERTY && setter != null) { JpaPropertyInfo i = new JpaPropertyInfo(getter, new MethodPropertyReader(getter), new MethodPropertyWriter(setter)); jpaProperties.put(propertyName, i); } } // scan all fields for (String propertyName : fields.keySet()) { Field field = fields.get(propertyName); // determine access type AccessType ac = accessType; Access access = field.getAnnotation(Access.class); if (access != null) { ac = access.value(); } if (ac != AccessType.FIELD) { continue; } Method getter = getters.get(propertyName); Method setter = setters.get(propertyName); // property reader final PropertyReader propertyReader; if (getter != null) { propertyReader = new MethodPropertyReader(getter); } else { propertyReader = new FieldPropertyReader(field); } // property writer final PropertyWriter propertyWriter; if (setter != null) { propertyWriter = new MethodPropertyWriter(setter); } else { propertyWriter = new FieldPropertyWriter(field); } jpaProperties.put(propertyName, new JpaPropertyInfo(field, propertyReader, propertyWriter)); } // find all properties end relations List<String> properties = new ArrayList<String>(); LinkedList<String> relations = new LinkedList<String>(); for (Map.Entry<String, JpaPropertyInfo> entry : jpaProperties.entrySet()) { final String propertyName = entry.getKey(); final JpaPropertyInfo propertyInfo = entry.getValue(); if (propertyInfo.isBasic()) { properties.add(propertyName); } else { OneToMany oneToMany = propertyInfo.getAccessibleObject().getAnnotation(OneToMany.class); ManyToMany manyToMany = propertyInfo.getAccessibleObject().getAnnotation(ManyToMany.class); if (oneToMany == null && manyToMany == null) { relations.addLast(propertyName); } else { // optimization for *ToMany : putting in front may reduce DB queries relations.addFirst(propertyName); } } } this.baseProperties = unmodifiableList(properties); this.relations = unmodifiableList(new ArrayList<String>(relations)); } /** * Process the class hierarchy. */ private void process(final Class<?> clazz) { if (clazz == null || clazz == Object.class || clazz.isInterface()) { return; } // process super class first process(clazz.getSuperclass()); // fields for (Field f : clazz.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) { continue; } fields.put(f.getName(), f); } // getters & setters for (Method m : clazz.getDeclaredMethods()) { if (Modifier.isStatic(m.getModifiers())) { continue; } String methodName = m.getName(); String propertyName = null; Map<String, Method> map = null; if (methodName.startsWith("get") && methodName.length() > 3 && m.getParameterTypes().length == 0) { propertyName = methodName.substring(3); map = getters; } else if (methodName.startsWith("is") && methodName.length() > 2 && m.getParameterTypes().length == 0) { propertyName = methodName.substring(2); map = getters; } else if (methodName.startsWith("set") && methodName.length() > 3 && m.getParameterTypes().length == 1) { propertyName = methodName.substring(3); map = setters; } if (propertyName != null && !propertyName.isEmpty()) { propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1); map.put(propertyName, m); } } } public Constructor<?> getConstructor() { return constructor; } public List<String> getBaseProperties() { return baseProperties; } public List<String> getRelations() { return relations; } public JpaPropertyInfo getPropertyInfo(String property) { return jpaProperties.get(property); } }