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

org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper 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 java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
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.ConcurrentMap;

import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.LdapDataEntry;
import org.springframework.core.SpringVersion;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.Filter;
import org.springframework.ldap.odm.annotations.DnAttribute;
import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
import org.springframework.ldap.odm.typeconversion.ConverterManager;
import org.springframework.ldap.odm.typeconversion.impl.ConversionServiceConverterManager;
import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl;
import org.springframework.ldap.support.LdapNameBuilder;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
 * Default implementation of {@link ObjectDirectoryMapper}. Unless you need to explicitly configure
 * converters there is typically no reason to explicitly consider yourself with this class.
 *
 * @author Paul Harvey <paul.at.pauls-place.me.uk>
 * @author Mattias Hellborg Arthursson
 * @since 2.0
 */
public class DefaultObjectDirectoryMapper implements ObjectDirectoryMapper {
	private static final Logger LOG = LoggerFactory.getLogger(DefaultObjectDirectoryMapper.class);

	// The converter manager to use to translate values between LDAP and Java
	private ConverterManager converterManager;

	private static final String OBJECT_CLASS_ATTRIBUTE="objectclass";
	private static final CaseIgnoreString OBJECT_CLASS_ATTRIBUTE_CI=new CaseIgnoreString(OBJECT_CLASS_ATTRIBUTE);


	public DefaultObjectDirectoryMapper() {
		converterManager = createDefaultConverterManager();
	}

	private static ConverterManager createDefaultConverterManager() {
		String springVersion = SpringVersion.getVersion();
		if(springVersion == null) {
			LOG.debug("Could not determine the Spring Version. Guessing > Spring 3.0. If this does not work, please ensure to explicitly set converterManager");
			return new ConversionServiceConverterManager();
		} else if(springVersion.compareTo("3.0") > 0) {
			return new ConversionServiceConverterManager();
		} else {
			return new ConverterManagerImpl();
		}
	}

	public void setConverterManager(ConverterManager converterManager) {
		this.converterManager = converterManager;
	}

	static final class EntityData {
		final ObjectMetaData metaData;
		final Filter ocFilter;

		private EntityData(ObjectMetaData metaData, Filter ocFilter) {
			this.metaData=metaData;
			this.ocFilter=ocFilter;
		}
	}

	// A map of managed classes to to meta data about those classes
	private final ConcurrentMap, EntityData> metaDataMap=new ConcurrentHashMap, EntityData>();

	private EntityData getEntityData(Class managedClass) {
		EntityData result = metaDataMap.get(managedClass);
		if (result == null) {
			return addManagedClass(managedClass);
		}
		return result;
	}

	@Override
	public String[] manageClass(Class clazz) {
		// This throws exception if data is invalid
		EntityData entityData = getEntityData(clazz);
		Set managedAttributeNames = new HashSet();
		// extract all relevant attributes
		for (Field field : entityData.metaData) {
			AttributeMetaData attributeMetaData = entityData.metaData.getAttribute(field);
			// skip transient fields
			if (attributeMetaData.isTransient()) {
				continue;
			}
			String[] attributesOfField = attributeMetaData.getAttributes();
			if (attributesOfField != null && attributesOfField.length > 0) {
				// attribute names are either given through annotation
				managedAttributeNames.addAll(Arrays.asList(attributesOfField));
			} else {
				// or implicitly by relying on the field name
				managedAttributeNames.add(field.getName());
			}
		}
		// always add the mandatory attribute objectclass (which is always used for the mapping)
		managedAttributeNames.add(OBJECT_CLASS_ATTRIBUTE);
		return managedAttributeNames.toArray(new String[managedAttributeNames.size()]);
	}

	/**
	 * Adds an {@link org.springframework.ldap.odm.annotations} annotated class to the set
	 * managed by this OdmManager.
	 *
	 * @param managedClass The class to add to the managed set.
	 */
	private EntityData addManagedClass(Class managedClass) {
		if (LOG.isDebugEnabled()) {
			LOG.debug(String.format("Adding class %1$s to managed set", managedClass));
		}

		// Extract the meta-data from the class
		ObjectMetaData metaData=new ObjectMetaData(managedClass);

		// Check we can construct the target type - it must have a zero argument public constructor
		try {
			managedClass.getConstructor();
		} catch (NoSuchMethodException e) {
			throw new InvalidEntryException(String.format(
					"The class %1$s must have a zero argument constructor to be an Entry", managedClass), e);
		}

		// Check we have all of the necessary converters for the class
		for (Field field : metaData) {
			AttributeMetaData attributeInfo = metaData.getAttribute(field);
			if (!attributeInfo.isTransient() && !attributeInfo.isId() && !(attributeInfo.isObjectClass())) {
				verifyConversion(managedClass, field, attributeInfo);
			}
		}

		// Filter so we only read the object classes supported by the managedClass
		AndFilter ocFilter = new AndFilter();
		for (CaseIgnoreString oc : metaData.getObjectClasses()) {
			ocFilter.and(new EqualsFilter(OBJECT_CLASS_ATTRIBUTE, oc.toString()));
		}

		EntityData newValue = new EntityData(metaData, ocFilter);
		EntityData previousValue = metaDataMap.putIfAbsent(managedClass, newValue);
		// Just in case someone beat us to it
		if(previousValue != null) {
			return previousValue;
		}

		return newValue;
	}

	private void verifyConversion(Class managedClass, Field field, AttributeMetaData attributeInfo) {
		Class jndiClass = attributeInfo.getJndiClass();
		Class javaClass = attributeInfo.getValueClass();
		if (!converterManager.canConvert(jndiClass, attributeInfo.getSyntax(), javaClass)) {
			throw new InvalidEntryException(String.format(
					"Missing converter from %1$s to %2$s, this is needed for field %3$s on Entry %4$s",
					jndiClass, javaClass, field.getName(), managedClass));
		}
		if (!attributeInfo.isReadOnly() && !converterManager.canConvert(javaClass, attributeInfo.getSyntax(), jndiClass)) {
			throw new InvalidEntryException(String.format(
					"Missing converter from %1$s to %2$s, this is needed for field %3$s on Entry %4$s",
					javaClass, jndiClass, field.getName(), managedClass));
		}
	}

	@Override
	public void mapToLdapDataEntry(Object entry, LdapDataEntry context) {
		ObjectMetaData metaData=getEntityData(entry.getClass()).metaData;

		Attribute objectclassAttribute = context.getAttributes().get(OBJECT_CLASS_ATTRIBUTE);
		if(objectclassAttribute == null || objectclassAttribute.size() == 0) {
			// Object classes are set from the metadata obtained from the @Entity annotation,
			// but only if this is a new entry.
			int numOcs=metaData.getObjectClasses().size();
			CaseIgnoreString[] metaDataObjectClasses=metaData.getObjectClasses().toArray(new CaseIgnoreString[numOcs]);

			String[] stringOcs=new String[numOcs];
			for (int ocIndex=0; ocIndex targetClass = attributeInfo.getJndiClass();
					// Multi valued?
					if (!attributeInfo.isCollection()) {
						populateSingleValueAttribute(entry, context, field, attributeInfo, targetClass);

					} else {
						// Multi-valued
						populateMultiValueAttribute(entry, context, field, attributeInfo, targetClass);

					}
				} catch (IllegalAccessException e) {
					throw new InvalidEntryException(String.format("Can't set attribute %1$s", attributeInfo.getName()),
							e);
				}
			}
		}
	}

	private void populateMultiValueAttribute(Object entry, LdapDataEntry context, Field field, AttributeMetaData attributeInfo, Class targetClass) throws IllegalAccessException {
		// We need to build up a list of of the values
		List attributeValues = new ArrayList();
		// Get the list of values
		Collection fieldValues = (Collection)field.get(entry);
		// Ignore null lists
		if (fieldValues != null) {
			for (final Object o : fieldValues) {
				// Ignore null values
				if (o != null) {
					attributeValues.add(converterManager.convert(o, attributeInfo.getSyntax(),
							targetClass));
				}
			}
			context.setAttributeValues(attributeInfo.getName().toString(), attributeValues.toArray());
		}
	}

	private void populateSingleValueAttribute(Object entry, LdapDataEntry context, Field field, AttributeMetaData attributeInfo, Class targetClass) throws IllegalAccessException {
		// Single valued - get the value of the field
		Object fieldValue = field.get(entry);
		// Ignore null field values
		if (fieldValue != null) {
			// Convert the field value to the required type and write it into the JNDI context
			context.setAttributeValue(attributeInfo.getName().toString(), converterManager.convert(fieldValue,
					attributeInfo.getSyntax(), targetClass));
		} else {
			context.setAttributeValue(attributeInfo.getName().toString(), null);
		}
	}

	@Override
	public  T mapFromLdapDataEntry(LdapDataEntry context, Class clazz) {
		if (LOG.isDebugEnabled()) {
			LOG.debug(String.format("Converting to Java Entry class %1$s from %2$s", clazz, context));
		}

		// The Java representation of the LDAP entry
		T result;

		ObjectMetaData metaData=getEntityData(clazz).metaData;

		try {
			// The result class must have a zero argument constructor
			result = clazz.newInstance();

			// Build a map of JNDI attribute names to values
			Map attributeValueMap = new HashMap();
			// Get a NamingEnumeration to loop through the JNDI attributes in the entry
			Attributes attributes = context.getAttributes();
			NamingEnumeration attributesEnumeration = attributes.getAll();
			// Loop through all of the JNDI attributes
			while (attributesEnumeration.hasMoreElements()) {
				Attribute currentAttribute = attributesEnumeration.nextElement();
				// Add the current attribute to the map keyed on the lowercased (case indep) id of the attribute
				attributeValueMap.put(new CaseIgnoreString(currentAttribute.getID()), currentAttribute);
			}


			// If this is the objectclass attribute then check that values correspond to the metadata we have
			// for the Java representation
			Attribute ocAttribute = attributeValueMap.get(OBJECT_CLASS_ATTRIBUTE_CI);
			if (ocAttribute != null) {
				// Get all object class values from the JNDI attribute
				Set objectClassesFromJndi = new HashSet();
				NamingEnumeration objectClassesFromJndiEnum = ocAttribute.getAll();
				while (objectClassesFromJndiEnum.hasMoreElements()) {
					objectClassesFromJndi.add(new CaseIgnoreString((String)objectClassesFromJndiEnum.nextElement()));
				}
				// OK - checks its the same as the meta-data we have
				if(!collectionContainsAll(objectClassesFromJndi, metaData.getObjectClasses())) {
					return null;
				}
			} else {
				throw new InvalidEntryException(String.format("No object classes were returned for class %1$s",
						clazz.getName()));
			}

			// Now loop through all the fields in the Java representation populating it with values from the
			// attributeValueMap
			for (Field field : metaData) {
				// Get the current field
				AttributeMetaData attributeInfo = metaData.getAttribute(field);
				// We deal with the Id field specially
				Name dn = context.getDn();
				if (!attributeInfo.isTransient() && !attributeInfo.isId()) {
					// Not the ID - but is is multi valued?
					if (!attributeInfo.isCollection()) {
						// No - its single valued, grab the JNDI attribute that corresponds to the metadata on the
						// current field
						populateSingleValueField(result, attributeValueMap, field, attributeInfo);
					} else {
						// We are dealing with a multi valued attribute
						populateMultiValueField(result, attributeValueMap, field, attributeInfo);
					}
				} else if(attributeInfo.isId()) { // The id field
					field.set(result, converterManager.convert(dn, attributeInfo.getSyntax(),
							attributeInfo.getValueClass()));
				}

				DnAttribute dnAttribute = attributeInfo.getDnAttribute();
				if(dnAttribute != null) {
					String dnValue;
					int index = dnAttribute.index();

					if(index != -1) {
						dnValue = LdapUtils.getStringValue(dn, index);
					} else {
						dnValue = LdapUtils.getStringValue(dn, dnAttribute.value());
					}
					field.set(result, dnValue);
				}
			}
		} catch (NamingException ne) {
			throw new InvalidEntryException(String.format("Problem creating %1$s from LDAP Entry %2$s",
					clazz, context), ne);
		} catch (IllegalAccessException iae) {
			throw new InvalidEntryException(String.format(
					"Could not create an instance of %1$s could not access field", clazz.getName()), iae);
		} catch (InstantiationException ie) {
			throw new InvalidEntryException(String.format("Could not instantiate %1$s", clazz), ie);
		}

		if (LOG.isDebugEnabled()) {
			LOG.debug(String.format("Converted object - %1$s", result));
		}

		return result;
	}

	private  void populateMultiValueField(T result, Map attributeValueMap, Field field, AttributeMetaData attributeInfo) throws NamingException, IllegalAccessException {
		// We need to build up a list of values
		Collection fieldValues = attributeInfo.newCollectionInstance();
		// Grab the attribute from the JNDI representation
		Attribute currentAttribute = attributeValueMap.get(attributeInfo.getName());
		// There is no guarantee that this attribute is present in the directory - so ignore nulls
		if (currentAttribute != null) {
			// Loop through the values of the JNDI attribute
			NamingEnumeration valuesEmumeration = currentAttribute.getAll();
			while (valuesEmumeration.hasMore()) {
				// Get the current value
				Object value = valuesEmumeration.nextElement();
				// Check the value is not null
				if (value != null) {
					// Convert the value to its Java representation and add it to our working list
					fieldValues.add(converterManager.convert(value, attributeInfo.getSyntax(),
							attributeInfo.getValueClass()));
				}
			}
		}
		// Now we need to set the List in to a Java object
		field.set(result, fieldValues);
	}

	private  void populateSingleValueField(T result, Map attributeValueMap, Field field, AttributeMetaData attributeInfo) throws NamingException, IllegalAccessException {
		Attribute attribute = attributeValueMap.get(attributeInfo.getName());
		// There is no guarantee that this attribute is present in the directory - so ignore nulls
		if (attribute != null) {
			// Grab the JNDI value
			Object value = attribute.get();
			// Check the value is not null
			if (value != null) {
				// Convert the JNDI value to its Java representation - this will throw if the
				// conversion fails
				Object convertedValue = converterManager.convert(value, attributeInfo.getSyntax(),
						attributeInfo.getValueClass());
				// Set it in the Java version
				field.set(result, convertedValue);
			}
		}
	}

	@Override
	public Name getId(Object entry) {
		try {
			return (Name) getIdField(entry).get(entry);
		} catch (Exception e) {
			throw new InvalidEntryException(String.format("Can't get Id field from Entry %1$s", entry),
					e);
		}
	}

	private Field getIdField(Object entry) {
		return getEntityData(entry.getClass()).metaData.getIdAttribute().getField();
	}

	@Override
	public void setId(Object entry, Name id) {
		try {
			getIdField(entry).set(entry, id);
		} catch (Exception e) {
			throw new InvalidEntryException(
					String.format("Can't set Id field on Entry %s to %s", entry, id), e);
		}
	}

	@Override
	public Name getCalculatedId(Object entry) {
		Assert.notNull(entry, "Entry must not be null");
		EntityData entityData = getEntityData(entry.getClass());
		if(entityData.metaData.canCalculateDn()) {
			Set dnAttributes = entityData.metaData.getDnAttributes();
			LdapNameBuilder ldapNameBuilder = LdapNameBuilder.newInstance(entityData.metaData.getBase());

			for (AttributeMetaData dnAttribute : dnAttributes) {
				Object dnFieldValue = ReflectionUtils.getField(dnAttribute.getField(), entry);
				if(dnFieldValue == null) {
					throw new IllegalStateException(
							String.format("DnAttribute for field %s on class %s is null; cannot build DN",
									dnAttribute.getField().getName(), entry.getClass().getName()));
				}

				ldapNameBuilder.add(dnAttribute.getDnAttribute().value(), dnFieldValue.toString());
			}

			return ldapNameBuilder.build();
		}

		return null;
	}

	@Override
	public Filter filterFor(Class clazz, Filter baseFilter) {
		Filter ocFilter = getEntityData(clazz).ocFilter;

		if(baseFilter == null) {
			return ocFilter;
		}

		AndFilter andFilter = new AndFilter();
		return andFilter.append(ocFilter).append(baseFilter);
	}

	@Override
	public String attributeFor(Class clazz, String fieldName) {
		try {
			Field field = clazz.getDeclaredField(fieldName);
			AttributeMetaData attributeMetaData =
					getEntityData(clazz).metaData.getAttribute(field);
			return attributeMetaData.getName().toString();
		} catch (NoSuchFieldException e) {
			throw new IllegalArgumentException(
					String.format("Field %s cannot be found in class %s", fieldName, clazz), e);
		}
	}

	// For testing purposes
	ConcurrentMap, EntityData> getMetaDataMap() {
		return metaDataMap;
	}

	static boolean collectionContainsAll(Collection collection, Set shouldBePresent) {
		for (Object o : shouldBePresent) {
			if(!collection.contains(o)) {
				return false;
			}
		}

		return true;
	}
}