/* * Copyright 2013 Gordon Burgett and individual contributors * * 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 org.xflatdb.xflat.db; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import org.xflatdb.xflat.Id; import org.xflatdb.xflat.XFlatException; import org.jdom2.Namespace; import org.jdom2.filter.Filters; import org.jdom2.xpath.XPathExpression; import org.jdom2.xpath.XPathFactory; /** * A helper class that accesses the IDs of an object. * @author gordon */ public class IdAccessor<T> { private final PropertyDescriptor idProperty; private final Field idField; private final Class<T> pojoType; public Class<T> getPojoType(){ return pojoType; } private XPathExpression<Object> alternateIdExpression = null; private IdAccessor(Class<T> pojoType, PropertyDescriptor idProperty, Field idField){ this.pojoType = pojoType; this.idProperty = idProperty; this.idField = idField; } private static ConcurrentHashMap<Class<?>, IdAccessor<?>> cachedAccessors = new ConcurrentHashMap<>(); /** * Gets the IdAccessor for the given pojo type. Id Accessors are cached * statically so that the reflection is only performed once. * @param <U> The type of the class for which to get the accessor. * @param pojoType The type of the class for which to get the accessor. * @return The accessor, which may have already been created and cached. */ public static <U> IdAccessor<U> forClass(Class<U> pojoType){ if(pojoType.isPrimitive() || String.class.equals(pojoType)){ return null; } IdAccessor<U> ret = (IdAccessor<U>)cachedAccessors.get(pojoType); if(ret != null){ return ret; } PropertyDescriptor idProp = null; Field idField = null; try{ Object idPropOrField = getIdPropertyOrField(pojoType); if(idPropOrField != null){ if(idPropOrField instanceof PropertyDescriptor){ idProp = (PropertyDescriptor)idPropOrField; } else{ idField = (Field)idPropOrField; idField.setAccessible(true); } } } catch(IntrospectionException ex){ throw new XFlatException("Cannot determine ID property of class " + pojoType.getName(), ex); } ret = new IdAccessor<>(pojoType, idProp, idField); if(ret.hasId()){ //see if there's an alternate ID expression. ret.alternateIdExpression = getAlternateId(ret.getIdPropertyAnnotation(Id.class)); } verify(ret, pojoType); cachedAccessors.putIfAbsent(pojoType, ret); return ret; } private static XPathExpression<Object> getAlternateId(Id idPropertyAnnotation){ if(idPropertyAnnotation == null) return null; String expression = idPropertyAnnotation.value(); if(expression == null || "".equals(expression)){ return null; } List<Namespace> namespaces = null; if(idPropertyAnnotation.namespaces() != null && idPropertyAnnotation.namespaces().length > 0){ for(String ns : idPropertyAnnotation.namespaces()){ if(!ns.startsWith("xmlns:")){ continue; } int eqIndex = ns.indexOf("="); if(eqIndex < 0 || eqIndex >= ns.length() - 1){ continue; } String prefix = ns.substring(6, eqIndex); String url = ns.substring(eqIndex + 1); if(url.startsWith("\"") || url.startsWith("'")) url = url.substring(1); if(url.endsWith("\"") || url.endsWith("'")) url = url.substring(0, url.length() - 1); if("".equals(prefix) || "".equals(url)) continue; if(namespaces == null) namespaces = new ArrayList<>(); namespaces.add(Namespace.getNamespace(prefix, url)); } } //compile it return XPathFactory.instance().compile(expression, Filters.fpassthrough(), null, namespaces == null ? Collections.EMPTY_LIST : namespaces); } private static Object getIdPropertyOrField(Class<?> pojoType) throws IntrospectionException{ List<PropertyDescriptor> descriptors = new ArrayList<>(); for(PropertyDescriptor p : Introspector.getBeanInfo(pojoType).getPropertyDescriptors()){ if(p.getReadMethod() == null || Object.class.equals(p.getReadMethod().getDeclaringClass())) continue; descriptors.add(p); } for(PropertyDescriptor p : descriptors){ if(p.getReadMethod().getAnnotation(Id.class) != null){ return p; } } List<Field> fields = new ArrayList<>(); while(!Object.class.equals(pojoType)){ for(Field f : pojoType.getDeclaredFields()){ if(Object.class.equals(f.getDeclaringClass())) continue; if((f.getModifiers() & Modifier.STATIC) == Modifier.STATIC || (f.getModifiers() & Modifier.FINAL) == Modifier.FINAL) continue; fields.add(f); } pojoType = pojoType.getSuperclass(); } //try fields marked with ID attribute for(Field f : fields){ if(f.getAnnotation(Id.class) != null){ return f; } } //try properties named ID for(PropertyDescriptor p : descriptors){ if("id".equalsIgnoreCase(p.getName())){ return p; } } //try fields named ID for(Field f : fields){ if("id".equalsIgnoreCase(f.getName())){ return f; } } return null; } private static void verify(IdAccessor accessor, Class<?> pojoType){ //if we don't have an ID, verify that the pojo uses reference equality. if(!accessor.hasId()){ Method eqMethod; Method hashCodeMethod; try { eqMethod = pojoType.getMethod("equals", Object.class); hashCodeMethod = pojoType.getMethod("hashCode"); } catch (NoSuchMethodException | SecurityException ex) { //should never happen throw new XFlatException("Unable to verify pojo " + pojoType, ex); } if(!Object.class.equals(eqMethod.getDeclaringClass()) || !Object.class.equals(hashCodeMethod.getDeclaringClass())) { //this is because our weak reference map that keeps track of IDs //for classes that don't have an Id property uses reference equality throw new XFlatException("Persistent objects that override " + "equals or hashCode must also declare an id field or property"); } } } /** * Indicates whether the POJO introspected by this accessor has an ID property. * @return true if an ID property or field was detected. */ public boolean hasId(){ return this.idProperty != null || this.idField != null; } public Class<?> getIdType(){ if(this.idProperty != null) { return this.idProperty.getPropertyType(); } if(this.idField != null){ return this.idField.getType(); } return null; } /** * Gets the value of the ID by accessing the ID property or field * on the object. * * If the ID is a JavaBean property, the property's getter is invoked via reflection. * If the ID is a field, the field is retrieved via reflection. * @param pojo The object whose ID to get. * @return The value of the object's ID property or field. * @throws IllegalAccessException * @throws InvocationTargetException * @see Method#invoke(java.lang.Object, java.lang.Object[]) * @see Field#get(java.lang.Object) */ public Object getIdValue(T pojo) throws IllegalAccessException, InvocationTargetException { if(this.idProperty != null){ return this.idProperty.getReadMethod().invoke(pojo); } if(this.idField != null){ return this.idField.get(pojo); } throw new UnsupportedOperationException("Cannot get ID value when object has no ID"); } /** * Sets the object's ID property or field to the given value. * * If the ID is a JavaBean property, the property's setter is invoked via reflection. * If the ID is a field, the field is set via reflection. * @param pojo The object whose ID should be set * @param id The new value of the ID * @throws IllegalAccessException * @throws InvocationTargetException * @see Method#invoke(java.lang.Object, java.lang.Object[]) * @see Field#set(java.lang.Object, java.lang.Object) */ public void setIdValue(T pojo, Object id) throws IllegalAccessException, InvocationTargetException { if(this.idProperty != null){ if(this.idProperty.getWriteMethod() != null) this.idProperty.getWriteMethod().invoke(pojo, id); return; } if(this.idField != null){ this.idField.set(pojo, id); return; } throw new UnsupportedOperationException("Cannot get ID value when object has no ID"); } public String getIdPropertyName(){ if(this.idProperty != null){ return this.idProperty.getName(); } if(this.idField != null){ return this.idField.getName(); } throw new UnsupportedOperationException("Cannot get field name when object has no ID"); } public <U extends Annotation> U getIdPropertyAnnotation(Class<U> annotationClass){ if(this.idProperty != null){ return this.idProperty.getReadMethod().getAnnotation(annotationClass); } if(this.idField != null){ return this.idField.getAnnotation(annotationClass); } throw new UnsupportedOperationException("Cannot get annotation when object has no ID"); } public XPathExpression<Object> getAlternateIdExpression(){ return alternateIdExpression; } }