/*
* 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;
}
}