All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.ldap.odm.core.impl.AttributeMetaData Maven / Gradle / Ivy

/*
 * Copyright 2005-2013 the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.ldap.odm.core.impl;

import org.springframework.ldap.UncategorizedLdapException;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.DnAttribute;
import org.springframework.ldap.odm.annotations.Id;
import org.springframework.ldap.odm.annotations.Transient;

import javax.naming.Name;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/*
 * Extract attribute meta-data from the @Attribute annotation, the @Id annotation
 * and via reflection.
 * 
 * @author Paul Harvey <paul.at.pauls-place.me.uk>
 */
/* package */ final class AttributeMetaData {
	private static final CaseIgnoreString OBJECT_CLASS_ATTRIBUTE_CI=new CaseIgnoreString("objectclass");
	
	// Name of the LDAP attribute from the @Attribute annotation
	private CaseIgnoreString name;
	
	// Syntax of the LDAP attribute from the @Attribute annotation
	private String syntax;

	// Whether this attribute is binary from the @Attribute annotation
	private boolean isBinary;

	// The Java field corresponding to this meta-data
	private final Field field;

	// The Java class of the field corresponding to this meta data
	// This is the actual scalar type meaning that if the field is 
	// List then the valueClass will be String
	private Class valueClass;

	// Is this field annotated @Id
	private boolean isId;

	// Is this field multi-valued represented by a List
	private boolean isCollection;

	private Class collectionClass;

	// Is this the objectClass attribute
	private boolean isObjectClass;

	private boolean isTransient = false;

	private boolean isReadOnly = false;

	private String[] attributes;

	private DnAttribute dnAttribute;

	// Extract information from the @Attribute annotation:
	// syntax, isBinary, isObjectClass and name.
	private boolean processAttributeAnnotation(Field field) {
		// Default to no syntax specified
		syntax = "";
		
		// Default to a String based attribute
		isBinary = false;
		
		// Default name of attribute to the name of the field
		name = new CaseIgnoreString(field.getName());
		
		// We have not yet found the @Attribute annotation
		boolean foundAnnotation=false;
		
		// Grab the @Attribute annotation
		Attribute attribute = field.getAnnotation(Attribute.class);

		List attrList = new ArrayList();
		// Did we find the annotation?
		if (attribute != null) {
			// Pull attribute name, syntax and whether attribute is binary
			// from the annotation 
			foundAnnotation=true;
			String localAttributeName = attribute.name();
			// Would be more efficient to use !isEmpty - but that then makes us Java 6 dependent
			if (localAttributeName != null && localAttributeName.length()>0) {
				name = new CaseIgnoreString(localAttributeName);
				attrList.add(localAttributeName);
			}
			syntax = attribute.syntax();
			isBinary = attribute.type() == Attribute.Type.BINARY;
			isReadOnly = attribute.readonly();
		}
		attributes = attrList.toArray(new String[attrList.size()]);
		
		isObjectClass=name.equals(OBJECT_CLASS_ATTRIBUTE_CI);
		
		return foundAnnotation;
	}
	
	// Extract reflection information from the field:
	// valueClass, isList
	private void determineFieldType(Field field) {
		// Determine the class of data stored in the field
		Class fieldType = field.getType();

		isCollection = Collection.class.isAssignableFrom(fieldType);

		valueClass=null;
		if (!isCollection) {
			// It's not a list so assume its single valued - so just take the field type
			valueClass = fieldType;
		} else {
			determineCollectionClass(fieldType);
			// It's multi-valued - so we need to look at the signature in
			// the class file to find the generic type - this is supported for class file
			// format 49 and greater which corresponds to java 5 and later.
			ParameterizedType paramType;
			try {
				paramType = (ParameterizedType)field.getGenericType();
			} catch (ClassCastException e) {
				throw new MetaDataException(String.format("Can't determine destination type for field %1$s in Entry class %2$s", 
						field, field.getDeclaringClass()), e);
			}
			Type[] actualParamArguments = paramType.getActualTypeArguments();
			if (actualParamArguments.length == 1) {
				if (actualParamArguments[0] instanceof Class) {
					valueClass = (Class)actualParamArguments[0];					
				} else {
					if (actualParamArguments[0] instanceof GenericArrayType) {
						// Deal with arrays
						Type type=((GenericArrayType)actualParamArguments[0]).getGenericComponentType();
						if (type instanceof Class) {
							valueClass=Array.newInstance((Class)type, 0).getClass();
						} 
					} 
				}
			} 
		}

		// Check we have been able to determine the value class
		if (valueClass==null) {
			throw new MetaDataException(String.format("Can't determine destination type for field %1$s in class %2$s", 
					field, field.getDeclaringClass()));
		}
	}

	@SuppressWarnings("unchecked")
	private void determineCollectionClass(Class fieldType) {
		if(fieldType.isInterface()) {
			if(Collection.class.equals(fieldType) || List.class.equals(fieldType)) {
				collectionClass = ArrayList.class;
			} else if(SortedSet.class.equals(fieldType)) {
				collectionClass = TreeSet.class;
			} else if(Set.class.isAssignableFrom(fieldType)) {
				collectionClass = LinkedHashSet.class;
			} else {
				throw new MetaDataException(String.format("Collection class %s is not supported", fieldType));
			}
		} else {
			collectionClass = (Class) fieldType;
		}
	}

	@SuppressWarnings("unchecked")
	public Collection newCollectionInstance() {
		try {
			return (Collection) collectionClass.newInstance();
		} catch (Exception e) {
			throw new UncategorizedLdapException("Failed to instantiate collection class", e);
		}
	}

	// Extract information from the @Id annotation:
	// isId
	private boolean processIdAnnotation(Field field, Class fieldType) {
		// Are we dealing with the Id field?
		isId=field.getAnnotation(Id.class)!=null;

		if (isId) {  
			// It must be of type Name or a subclass of that of
			if (!Name.class.isAssignableFrom(fieldType)) {
				throw new MetaDataException(
						String.format("The id field must be of type javax.naming.Name or a subclass that of in Entry class %1$s",
								field.getDeclaringClass()));
			}
		}
		
		return isId;
	}
	
	// Extract meta-data from the given field
	public AttributeMetaData(Field field) {
		this.field=field;

		this.dnAttribute = field.getAnnotation(DnAttribute.class);
		if(this.dnAttribute != null && !field.getType().equals(String.class)) {
			throw new MetaDataException(String.format("%s is of type %s, but only String attributes can be declared as @DnAttributes",
					field.toString(),
					field.getType().toString()));
		}

		Transient transientAnnotation = field.getAnnotation(Transient.class);
		if(transientAnnotation != null) {
			this.isTransient = true;
			return;
		}

		// Reflection data
		determineFieldType(field);


		// Data from the @Attribute annotation
		boolean foundAttributeAnnotation=processAttributeAnnotation(field);

		// Data from the @Id annotation
		boolean foundIdAnnoation=processIdAnnotation(field, valueClass);

		// Check that the field has not been annotated with both @Attribute and with @Id
		if (foundAttributeAnnotation && foundIdAnnoation) {
			throw new MetaDataException(
					String.format("You may not specifiy an %1$s annoation and an %2$s annotation on the same field, error in field %3$s in Entry class %4$s",
							Id.class, Attribute.class, field.getName(), field.getDeclaringClass()));
		}
		
		// If this is the objectclass attribute then it must be of type List
		if (isObjectClass() && (!isCollection() || valueClass!=String.class)) {
			throw new MetaDataException(String.format("The type of the objectclass attribute must be List in classs %1$s",
					field.getDeclaringClass()));
		}
	}


	public String getSyntax() {
		return syntax;
	}

	public boolean isBinary() {
		return isBinary;
	}

	public Field getField() {
		return field;
	}

	public CaseIgnoreString getName() {
		return name;
	}
	
	public boolean isCollection() {
		return isCollection;
	}

	public boolean isId() {
		return isId;
	}

	public boolean isReadOnly() {
		return isReadOnly;
	}

	public boolean isTransient() {
		return isTransient;
	}

	public DnAttribute getDnAttribute() {
		return dnAttribute;
	}

	public boolean isDnAttribute() {
		return dnAttribute != null;
	}

	public boolean isObjectClass() {
		return isObjectClass;
	}

	public Class getValueClass() {
		return valueClass;
	}

	public String[] getAttributes() {
		return attributes;
	}

	public Class getJndiClass() {
		if(isBinary()) {
			return byte[].class;
		} else if(Name.class.isAssignableFrom(valueClass)) {
			return Name.class;
		} else {
			return String.class;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return String.format("name=%1$s | field=%2$s | valueClass=%3$s | syntax=%4$s| isBinary=%5$s | isId=%6$s | isReadOnly=%7$s |  isList=%8$s | isObjectClass=%9$s",
				getName(), getField(), getValueClass(), getSyntax(), isBinary(), isId(), isReadOnly(), isCollection(), isObjectClass());
	}
}