You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by he...@apache.org on 2007/12/21 17:03:49 UTC

svn commit: r606228 [3/4] - in /directory/sandbox/hennejg/odm/trunk/src: ./ main/ main/java/ main/java/org/ main/java/org/apache/ main/java/org/apache/directory/ main/java/org/apache/directory/odm/ main/java/org/apache/directory/odm/auth/ main/resource...

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/TypeMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/TypeMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/TypeMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/TypeMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,1169 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.Name;
+import javax.naming.NameAlreadyBoundException;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The TypeMapping handles all mapping related tasks for one mapped class.
+ * 
+ * @author levigo
+ */
+public class TypeMapping implements Cloneable {
+	private static final Logger logger = LoggerFactory
+			.getLogger(TypeMapping.class);
+
+	public enum SearchScope {
+		OBJECT(SearchControls.OBJECT_SCOPE), ONELEVEL(SearchControls.ONELEVEL_SCOPE), SUBTREE(
+				SearchControls.SUBTREE_SCOPE);
+
+		private final int sc;
+
+		SearchScope(int sc) {
+			this.sc = sc;
+		}
+
+		public int getScope() {
+			return sc;
+		}
+	}
+
+	/**
+	 * The type's attributes.
+	 */
+	protected List<AttributeMapping> attributes = new ArrayList<AttributeMapping>();
+
+	/**
+	 * Other mapped attributes pointing back to this type. Not currently used.
+	 */
+	private List<AttributeMapping> referrers = new ArrayList<AttributeMapping>();
+
+	/**
+	 * The base RDN where objects of the type get stored by default. May be
+	 * overridden by specifying a base RDN as an argument to the load/list/save
+	 * methods.
+	 */
+	private final String baseRDN;
+
+	/**
+	 * This marker indicates an unchanged attribute, whatever the current value
+	 * may be. Used by AttributeMappings to communicate an unchanged attribute
+	 * during dehydration without having to know the current value.
+	 */
+	protected static final Object ATTRIBUTE_UNCHANGED_MARKER = "�$%&/()==UNCHANGED";
+
+	/**
+	 * The cached constructor for the type. Every mapped type must implement a
+	 * public default constructor.
+	 */
+	private Constructor constructor;
+
+	/**
+	 * The parent Mapping.
+	 */
+	private Mapping mapping;
+
+	/**
+	 * The class of instances mapped by this mapping.
+	 */
+	private final Class modelClass;
+
+	/**
+	 * The key class of this type.
+	 */
+	private final String keyClass;
+
+	/**
+	 * The LDAP objectClasses used by elements of the type.
+	 */
+	// private final String objectClasses[];
+	private String objectClasses[];
+
+	/**
+	 * The (Java-) attribute which holds the DN.
+	 */
+	private AttributeMapping dnAttribute;
+
+	/**
+	 * The (Java-) attribute which holds the RDN.
+	 */
+	private AttributeMapping rdnAttribute;
+
+	/**
+	 * The search filter used to search for objects of the mapped type below the
+	 * base dn. May be overridden by specifying a filter as an argument to the
+	 * load method.
+	 */
+	private final String searchFilter;
+	/**
+	 * The scope to use when listing objects of the mapped type.
+	 * 
+	 * @see SearchControls#setSearchScope(int)
+	 */
+	// private SearchScope defaultScope = SearchScope.ONELEVEL;
+	private SearchScope defaultScope = SearchScope.SUBTREE;
+
+	/**
+	 * The connection descriptor to use for this mapping.
+	 */
+	private DirectoryFacade directoryFacade;
+
+	private Name defaultBaseName;
+
+	public TypeMapping(String className, String baseRDN, String searchFilter,
+			String objectClasses, String keyClass) throws Exception {
+		try {
+			this.modelClass = Class.forName(className);
+			this.baseRDN = baseRDN;
+			this.searchFilter = searchFilter;
+			this.objectClasses = null != objectClasses ? objectClasses
+					.split("\\s*,\\s*") : new String[]{};
+			this.keyClass = keyClass;
+		} catch (Exception e) {
+			logger.error("Can't construct TypeMapping for class " + className, e);
+			throw e;
+		}
+	}
+
+	/**
+	 * @param mapping
+	 * @throws NoSuchMethodException
+	 */
+	public void add(AttributeMapping attributeMapping) {
+		attributeMapping.setTypeMapping(this);
+		attributes.add(attributeMapping);
+	}
+
+	/**
+	 * @return
+	 * @throws DirectoryException
+	 * @throws InvocationTargetException
+	 * @throws IllegalAccessException
+	 * @throws InstantiationException
+	 * @throws NoSuchMethodException
+	 * @throws IllegalArgumentException
+	 * @throws SecurityException
+	 */
+	public Object create() throws DirectoryException {
+		try {
+			final Object instance = createInstance();
+
+			rdnAttribute.initNewInstance(instance);
+			for (final AttributeMapping am : attributes)
+				am.initNewInstance(instance);
+
+			return instance;
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't create instance of " + modelClass);
+		}
+	}
+
+	/**
+	 * Create an empty object instance for this mapped type.
+	 * 
+	 * @return new, unhydrated instance
+	 * @throws Exception
+	 */
+	private Object createInstance() throws Exception {
+		final Constructor c = getConstructor();
+		final Object newInstance = c.newInstance(new Object[]{});
+
+		return newInstance;
+	}
+
+	/**
+	 * Create and hydrate an object instance from a set of attributes.
+	 * 
+	 * @param dn object's DN
+	 * @param a attributes used to hydrate the object
+	 * @param tx current transaction
+	 * @return
+	 * @throws Exception
+	 */
+	Object createInstanceFromAttributes(String dn, Attributes a, Transaction tx)
+			throws Exception {
+		final Object o = createInstance();
+		setDN(dn, o);
+		hydrateInstance(a, o, tx);
+
+		return o;
+	}
+
+	/**
+	 * Hydrate an object instance from a set of attributes.
+	 * 
+	 * @param a attributes used to hydrate the object
+	 * @param o object to hydrate
+	 * @param tx current transaction
+	 * @throws DirectoryException
+	 */
+	private void hydrateInstance(Attributes a, Object o, Transaction tx)
+			throws DirectoryException {
+		// map RDN
+		rdnAttribute.hydrate(o, a, tx);
+
+		// map all other attributes
+		for (final Object element : attributes) {
+			final AttributeMapping am = (AttributeMapping) element;
+			am.hydrate(o, a, tx);
+		}
+	}
+
+	/**
+	 * @param dn
+	 * @param o
+	 * @return
+	 * @throws DirectoryException
+	 */
+	private Object setDN(String dn, Object o) throws DirectoryException {
+		return dnAttribute.setValue(o, dn);
+	}
+
+	/**
+	 * @return
+	 */
+	public String getBaseRDN() {
+		return baseRDN;
+	}
+
+	/**
+	 * @return
+	 * @throws NoSuchMethodException
+	 * @throws SecurityException
+	 */
+	private Constructor getConstructor() throws SecurityException,
+			NoSuchMethodException {
+		if (null == constructor)
+			constructor = modelClass.getConstructor(new Class[]{});
+		return constructor;
+	}
+
+	public Mapping getMapping() {
+		return mapping;
+	}
+
+	/**
+	 * @return
+	 */
+	public Class getMappedType() {
+		return modelClass;
+	}
+
+	/**
+	 * List object of the mapped type for the given base DN, search filter and
+	 * scope.
+	 * 
+	 * @param filter the search filter
+	 * @param searchBase the search base DN
+	 * @param scope the search scope
+	 * @param tx the current transaction
+	 * @return
+	 * @throws DirectoryException
+	 */
+	public Set list(Filter filter, String searchBase, SearchScope scope,
+			Transaction tx) throws DirectoryException {
+		try {
+			final DirContext ctx = tx.getContext(directoryFacade);
+
+			// construct filter. if filter is set, join this type's filter with
+			// the supplied one.
+			String applicableFilter = searchFilter;
+			Object args[] = null;
+
+			if (null != filter) {
+				applicableFilter = "(&" + searchFilter + filter.getExpression(0) + ")";
+				args = filter.getArgs();
+
+				// FIXME: use Apache DS filter parser to properly upper-case the search
+				// expression
+
+				// if (directoryFacade.guessDirectoryType()
+				// .requiresUpperCaseRDNAttributeNames())
+				// // ...
+			}
+
+			// the dn will frequently be a descendant of the ctx's name. If this
+			// is the case, the prefix is removed, because search() expects
+			// a relative name.
+			if (null == searchBase)
+				searchBase = null != baseRDN ? baseRDN : "";
+
+			final Name searchBaseName = directoryFacade.makeRelativeName(searchBase);
+
+			// we want or results to carry absolute names. This is where
+			// they are rooted.
+			final Name resultBaseName = directoryFacade.makeAbsoluteName(searchBase);
+
+			if (logger.isDebugEnabled())
+				logger.debug("listing objects of " + modelClass + " for base="
+						+ searchBaseName + ", filter=" + filter);
+
+			final SearchControls sc = new SearchControls();
+			sc.setSearchScope(null != scope ? scope.getScope() : defaultScope
+					.getScope());
+
+			final Set results = new HashSet();
+			try {
+				NamingEnumeration<SearchResult> ne;
+
+				DiropLogger.LOG.logSearch(searchBase, applicableFilter, args, sc,
+						"list objects");
+
+				ne = ctx.search(searchBaseName, applicableFilter, args, sc);
+
+				try {
+					while (ne.hasMore()) {
+						final SearchResult result = ne.next();
+
+						// we want an absolute element name. Unfortunately,
+						// result.getNameInNamespace() is 1.5+ only, so we've
+						// got to work this out ourselves.
+						Name elementName = directoryFacade.getNameParser().parse(
+								result.getName());
+
+						// FIX for A-DS bug: name isn't relative but should be.
+						if (result.isRelative() && !elementName.startsWith(resultBaseName))
+							elementName = elementName.addAll(0, resultBaseName);
+
+						// got it in the tx cache?
+						Object instance = tx.getCacheEntry(elementName);
+						if (null == instance) {
+							final Attributes a = result.getAttributes();
+							instance = createInstanceFromAttributes(elementName.toString(),
+									a, tx);
+
+							// cache the object
+							tx.putCacheEntry(this, elementName, instance, a);
+						}
+
+						results.add(instance);
+					}
+				} finally {
+					// close the enumeration before cascading the load.
+					ne.close();
+				}
+
+				for (final Object o : results)
+					for (final AttributeMapping am : attributes)
+						// for (AttributeMapping am : attrs) {
+						am.cascadePostLoad(o, tx);
+
+			} catch (final NameNotFoundException e) {
+				logger.warn("NameNotFoundException listing objects of " + modelClass
+						+ " for base=" + searchBaseName + ". Returning empty set instead.");
+			}
+			return results;
+
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't list objects for type " + modelClass,
+					e);
+		}
+	}
+
+	/**
+	 * Load an object of the mapped type from the given DN.
+	 * 
+	 * @param tx the current transaction
+	 * @param dn the object's DN
+	 * @throws NamingException
+	 * @throws DirectoryException
+	 */
+	public Object load(String dn, Transaction tx) throws DirectoryException {
+		try {
+			if (null == dn)
+				dn = baseRDN;
+
+			// make the dn absolute, even if it was relative.
+			final DirContext ctx = tx.getContext(directoryFacade);
+
+			final Name targetName = directoryFacade.makeAbsoluteName(dn);
+
+			// got it in the tx cache?
+			final Object cached = tx.getCacheEntry(targetName);
+			if (null != cached)
+				return cached;
+
+			// seems like we've got to load it.
+			if (logger.isDebugEnabled())
+				logger.debug("loading object of " + modelClass + " for dn: "
+						+ targetName);
+
+			// FIXME: use lookup() instead of search
+			final SearchControls sc = new SearchControls();
+			sc.setSearchScope(SearchControls.OBJECT_SCOPE);
+
+			Object o = null;
+
+			// search() expects a base name relative to the ctx.
+			final Name searchName = directoryFacade.makeRelativeName(dn);
+
+			DiropLogger.LOG.logSearch(dn, searchFilter, null, sc,
+					"loading single object");
+
+			final NamingEnumeration<SearchResult> ne = ctx.search(searchName,
+					searchFilter, null, sc);
+
+			try {
+				if (!ne.hasMore())
+					throw new NameNotFoundException("No object for the given dn found.");
+
+				final SearchResult result = ne.nextElement();
+
+				if (ne.hasMore())
+					// scope=OBJECT_SCOPE!
+					throw new DirectoryException("More than one result return for query");
+
+				final Attributes a = result.getAttributes();
+				o = createInstanceFromAttributes(targetName.toString(), a, tx);
+
+				tx.putCacheEntry(this, targetName, o, a);
+			} finally {
+				// close the enumeration before cascading the load.
+				ne.close();
+			}
+
+			for (final AttributeMapping am : attributes)
+				am.cascadePostLoad(o, tx);
+
+			return o;
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't load object", e);
+		}
+	}
+
+	/**
+	 * @param o
+	 * @param transaction
+	 * @param baseDN2
+	 * @throws DirectoryException
+	 */
+	public void save(Object o, String baseDN, Transaction tx)
+			throws DirectoryException {
+		assert o.getClass().equals(modelClass);
+
+		if (getDirectoryFacade().isReadOnly())
+			throw new DirectoryException("Directory for " + o + " is read only");
+
+		// break cycles
+		if (tx.didAlreadyProcessEntity(o))
+			return;
+		tx.addEntity(o);
+
+		try {
+			final DirContext ctx = tx.getContext(directoryFacade);
+
+			// if the object has already got a DN set, we update it. Otherwise
+			// we save a new one.
+			final String dn = getDN(o);
+			Name name = null;
+			if (null == dn)
+				try {
+					saveNewObject(o, ctx, baseDN, tx);
+
+					return;
+				} catch (final NameAlreadyBoundException e) {
+					// The object's dn wasn't set. However, its
+					// RDN may have pointed to an existing object.
+					// Fall through to update mode.
+					name = fillEmptyDN(o, ctx, baseDN);
+					if (logger.isDebugEnabled())
+						logger
+								.debug("Caught NameAlreadyBoundException on saveNewObject for "
+										+ name + ". trying update instead.");
+				}
+
+			// if the target name wasn't provided by the fall-through above,
+			// build
+			// it based on the object's dn attribute.
+			// the dn will frequently be a descendant of the ctx's name. If this
+			// is the case, the prefix is removed.
+			if (null == name)
+				name = directoryFacade.makeRelativeName(dn);
+
+			try {
+				DiropLogger.LOG.logGetAttributes(name, null, "save object");
+
+				final Attributes currentAttributes = ctx.getAttributes(name);
+				updateObject(o, ctx, name, currentAttributes, tx);
+				return;
+			} catch (final NameNotFoundException e) {
+				throw new DirectoryException("Object to be updated no longer exists");
+			}
+		} catch (final DirectoryException e) {
+			throw e;
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't save object", e);
+		}
+	}
+
+	/**
+	 * @param o
+	 * @param ctx
+	 * @param baseDN
+	 * @param tx
+	 * @throws NamingException
+	 * @throws DirectoryException
+	 * @throws InvalidNameException
+	 * @throws NamingException
+	 * @throws DirectoryException
+	 */
+	private void saveNewObject(Object o, DirContext ctx, String baseDN,
+			Transaction tx) throws InvalidNameException, DirectoryException,
+			NamingException {
+		final Name targetName = fillEmptyDN(o, ctx, baseDN);
+
+		// perform cascading of stuff which has to be done before the new object
+		// can been saved.
+		for (final AttributeMapping attributeMapping : attributes)
+			attributeMapping.cascadePreSave(o, tx);
+
+		final BasicAttributes a = new BasicAttributes();
+		rdnAttribute.dehydrate(o, a);
+
+		fillAttributes(o, a);
+
+		DiropLogger.LOG.logAdd(getDN(o), a, "save new object");
+		ctx.bind(targetName, null, a);
+
+		// cache new object
+		tx.putCacheEntry(this, getDirectoryFacade().makeAbsoluteName(targetName),
+				o, a);
+
+		// perform cascading of stuff which has to be done after the new object
+		// has been saved.
+		try {
+			for (final AttributeMapping attributeMapping : attributes)
+				attributeMapping.cascadePostSave(o, tx, ctx);
+		} catch (final DirectoryException t) {
+			// rollback. FIXME: use transaction to do the dirty work
+			try {
+				DiropLogger.LOG.logDelete(targetName, "delete due to rollback");
+				ctx.destroySubcontext(targetName);
+			} catch (final Throwable u) {
+				// ignore
+			}
+			throw t;
+		}
+	}
+
+	/**
+	 * @param o
+	 * @param ctx
+	 * @param baseDN
+	 * @return
+	 * @throws DirectoryException
+	 * @throws NamingException
+	 * @throws InvalidNameException
+	 */
+	private Name fillEmptyDN(Object o, DirContext ctx, String baseDN)
+			throws DirectoryException, NamingException, InvalidNameException {
+		// the dn will frequently be a descendant of the ctx's name. If this
+		// is the case, the prefix is removed.
+
+		if (null == baseDN)
+			baseDN = this.baseRDN;
+
+		if (null == baseDN && getDirectoryFacade().isReadOnly())
+			baseDN = "";
+
+		if (null == baseDN)
+			throw new DirectoryException(
+					"Can't save object: don't know where to save it to");
+
+		final Name name = directoryFacade.makeRelativeName(baseDN);
+
+		final Object rdnValue = rdnAttribute.getValue(o);
+		if (null == rdnValue)
+			throw new DirectoryException(
+					"Can't save new instance: attribute for RDN (" + rdnAttribute
+							+ ") not set.");
+
+		// add rdn
+		name.addAll(directoryFacade.getNameParser().parse(
+				rdnAttribute.fieldName + "=" + rdnValue));
+
+		setDN(directoryFacade.makeAbsoluteName(name).toString(), o);
+
+		return name;
+	}
+
+	/**
+	 * @param mapping
+	 */
+	void setMapping(Mapping mapping) {
+		if (null != this.mapping)
+			this.mapping.remove(this);
+
+		this.mapping = mapping;
+	}
+
+	/**
+	 * @param mapping
+	 * @throws NoSuchMethodException
+	 */
+	public void setRDNAttribute(AttributeMapping rdnAttribute) {
+		if (!dnAttribute.getFieldType().equals(String.class))
+			throw new IllegalArgumentException(
+					"The RDN Attribute must be of type string");
+
+		rdnAttribute.setTypeMapping(this);
+		this.rdnAttribute = rdnAttribute;
+	}
+
+	public AttributeMapping getRDNAttribute() {
+		return this.rdnAttribute;
+	}
+
+	/*
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return "[TypeMapping class=" + modelClass + ", baseDN=" + baseRDN
+				+ ", filter=" + searchFilter + "]";
+	}
+
+	/**
+	 * @param o
+	 * @param ctx
+	 * @param targetName
+	 * @param currentAttributes
+	 * @param tx
+	 * @throws DirectoryException
+	 */
+	private void updateObject(Object o, DirContext ctx, Name targetName,
+			Attributes currentAttributes, Transaction tx) throws DirectoryException,
+			NamingException {
+		if (getDirectoryFacade().isReadOnly())
+			throw new DirectoryException("Directory for " + o + " is read only");
+
+		Name targetDN = getDirectoryFacade().makeAbsoluteName(targetName);
+
+		tx.purgeCacheEntry(targetDN);
+
+		try {
+			final BasicAttributes newAttributes = new BasicAttributes();
+
+			final Object rdn = rdnAttribute.dehydrate(o, newAttributes);
+			if (null == rdn)
+				throw new DirectoryException("Can't save new instance: "
+						+ "attribute for RDN (" + rdnAttribute + ") not set.");
+
+			if (!rdn.equals(currentAttributes.get(rdnAttribute.fieldName).get())) {
+				// ok, go for a rename!
+				targetName = renameObject(targetName, ctx, rdn, o, tx, newAttributes);
+
+				// dn has changed as well...
+				targetDN = getDirectoryFacade().makeAbsoluteName(targetName);
+			}
+
+			fillAttributes(o, newAttributes);
+
+			final List<ModificationItem> mods = new LinkedList<ModificationItem>();
+
+			// remove cleared Attributes
+			if (currentAttributes.size() > 0) {
+				final Attributes clearedAttributes = getClearedAttributes(
+						(BasicAttributes) newAttributes.clone(),
+						(Attributes) currentAttributes.clone());
+
+				if (clearedAttributes.size() > 0) {
+					final NamingEnumeration<Attribute> enmAttribute = (NamingEnumeration<Attribute>) clearedAttributes
+							.getAll();
+
+					while (enmAttribute.hasMore()) {
+						final Attribute clearedAttribute = enmAttribute.next();
+						mods.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
+								clearedAttribute));
+
+						if (logger.isDebugEnabled())
+							logger.debug("The value of following Attribute will be cleared: "
+									+ clearedAttribute);
+					}
+				}
+			}
+
+			// updates, adds
+			final NamingEnumeration<Attribute> ne = newAttributes.getAll();
+			try {
+				while (ne.hasMore()) {
+					final Attribute newValues = ne.next();
+
+					final String id = newValues.getID();
+					if (id.equalsIgnoreCase("objectclass")) {
+						currentAttributes.remove(id);
+						continue;
+					}
+
+					updateAttributes(currentAttributes, currentAttributes.get(id),
+							newValues, mods, o);
+				}
+			} finally {
+				ne.close();
+			}
+
+			// execute the modifications
+			if (mods.size() > 0) {
+				final ModificationItem mi[] = mods.toArray(new ModificationItem[mods
+						.size()]);
+				DiropLogger.LOG.logModify(targetName, mi, "update object");
+				ctx.modifyAttributes(targetName, mi);
+			}
+
+			tx.putCacheEntry(this, targetDN, o, newAttributes);
+
+			// perform cascading of stuff which has to be done after the new
+			// object has been saved.
+			for (final AttributeMapping attributeMapping : attributes)
+				attributeMapping.cascadePostSave(o, tx, ctx);
+		} catch (final DirectoryException e) {
+			throw e;
+		} catch (final Throwable e) {
+			throw new DirectoryException("Can't marshal instance of " + modelClass, e);
+		}
+	}
+
+	private Name renameObject(Name oldName, DirContext ctx, Object rdn, Object o,
+			Transaction tx, BasicAttributes attrib) throws NamingException,
+			DirectoryException {
+		final Name newName = oldName.getPrefix(oldName.size() - 1).add(
+				rdnAttribute.fieldName + "=" + rdn);
+
+		final String oldDN = getDN(o);
+		final String newDN = ((Name) getDirectoryFacade().getBaseDNName().clone())
+				.addAll(newName).toString();
+
+		DiropLogger.LOG.logModRDN(oldName, newName, "rename object");
+
+		ctx.rename(oldName, newName);
+		getMapping().updateReferences(tx, oldDN, newDN);
+
+		// and tell the object about the new dn
+		setDN(newDN, o);
+
+		// perform cascading of stuff which has to be done after the
+		// new object has been saved.
+		for (final AttributeMapping attributeMapping : attributes)
+			attributeMapping.cascadeRDNChange(o, oldDN, newDN, tx);
+
+		// let the rdn attribute alone!
+		attrib.remove(rdnAttribute.fieldName);
+
+		return oldName;
+	}
+
+	protected void updateAttributes(Attributes currentAttributes,
+			Attribute currentValues, Attribute newValues,
+			List<ModificationItem> mods, Object o) throws NamingException,
+			DirectoryException {
+		final String id = newValues.getID();
+		if (currentValues != null) {
+			// use identity comparison for unchanged marker!
+			if (newValues.size() == 1
+					&& newValues.get(0) == ATTRIBUTE_UNCHANGED_MARKER)
+				currentAttributes.remove(id);
+			else {
+				if (!areAttributesEqual(newValues, currentValues))
+					mods
+							.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, newValues));
+				currentAttributes.remove(id);
+			}
+		} else if (currentValues == null && newValues != null)
+			mods.add(new ModificationItem(DirContext.ADD_ATTRIBUTE, newValues));
+	}
+
+	private Attributes getClearedAttributes(BasicAttributes nowAttributes,
+			Attributes ldapAttributes) throws NamingException {
+
+		final Attributes cleared = new BasicAttributes();
+
+		// ignore objectClasses
+		nowAttributes.remove("objectClass");
+		ldapAttributes.remove("objectClass");
+
+		final NamingEnumeration<String> nowIDs = nowAttributes.getIDs();
+
+		while (nowIDs.hasMore()) {
+			final String id = nowIDs.next();
+			ldapAttributes.remove(id);
+		}
+
+		final Set<String> attr = new HashSet<String>();
+
+		for (final AttributeMapping am : attributes) {
+			if (am instanceof ManyToManyMapping)
+				continue;
+			attr.add(am.fieldName);
+		}
+
+		final NamingEnumeration<String> ldapIDs = ldapAttributes.getIDs();
+
+		while (ldapIDs.hasMore()) {
+			final String id = ldapIDs.next();
+			for (final String rightID : attr)
+				if (rightID.equalsIgnoreCase(id))
+					cleared.put(ldapAttributes.get(id));
+		}
+
+		return cleared;
+	}
+
+	/**
+	 * @param a1
+	 * @param a2
+	 * @return
+	 * @throws NamingException
+	 */
+	private boolean areAttributesEqual(Attribute a1, Attribute a2)
+			throws NamingException {
+		if (!a1.getID().equalsIgnoreCase(a2.getID()))
+			return false;
+
+		if (a1.get() == null && a2.get() == null)
+			return true;
+
+		if (a1.get() == null || a2.get() == null)
+			return false;
+
+		if (a1.size() != a2.size())
+			return false;
+
+		for (int i = 0; i < a1.size(); i++)
+			if (a1.get() instanceof byte[])
+				return Arrays.equals((byte[]) a1.get(), (byte[]) a2.get());
+			else
+				return a1.get(i).equals(a2.get(i));
+		return true;
+	}
+
+	/**
+	 * @param o
+	 * @param a
+	 * @throws DirectoryException
+	 * @throws NamingException
+	 */
+	private void fillAttributes(Object o, BasicAttributes a)
+			throws DirectoryException, NamingException {
+
+		// map all other attributes
+		for (final AttributeMapping attributeMapping : attributes)
+			attributeMapping.dehydrate(o, a);
+
+		// add object classes
+		final Attribute objectClassesAttribute = new BasicAttribute("objectClass");
+		for (final String oc : objectClasses)
+			objectClassesAttribute.add(oc);
+		a.put(objectClassesAttribute);
+	}
+
+	/**
+	 * @param o
+	 * @param transaction
+	 * @return
+	 * @throws DirectoryException
+	 * @throws NamingException
+	 */
+	public boolean delete(Object o, Transaction tx) throws DirectoryException {
+		if (getDirectoryFacade().isReadOnly())
+			throw new DirectoryException("Directory for " + o + " is read only");
+
+		// break cycles
+		if (tx.didAlreadyProcessEntity(o))
+			return true;
+		tx.addEntity(o);
+
+		final String dn = getDN(o);
+		if (null == dn)
+			throw new DirectoryException(
+					"Can't delete this object: no DN (mayby it wasn't saved before?)");
+		try {
+			final DirContext ctx = tx.getContext(directoryFacade);
+			// checkMutable(ctx);
+			// the dn will frequently be a descendant of the ctx's name. If
+			// this
+			// is the case, the prefix is removed.
+			final Name targetName = directoryFacade.makeRelativeName(dn);
+
+			// remove from cache
+			tx.purgeCacheEntry(targetName);
+
+			deleteRecursively(ctx, targetName, tx, "delete object");
+
+			getMapping().updateReferences(tx, dn, null);
+
+			try {
+				// perform cascading of stuff which has to be done after the
+				// object has been deleted.
+				for (final AttributeMapping attributeMapping : attributes)
+					attributeMapping.cascadeDelete(targetName, tx);
+			} catch (final DirectoryException e) {
+				logger.error("Exception during cascade post RDN change", e);
+			}
+			return true;
+
+		} catch (final NameNotFoundException e) {
+			logger.warn("Object to be deleted was not actually found.");
+			return false;
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't load object", e);
+		}
+	}
+
+	/**
+	 * @param ctx
+	 * @param targetName
+	 * @param tx
+	 * @param comment TODO
+	 * @throws NamingException
+	 */
+	static void deleteRecursively(DirContext ctx, Name targetName,
+			Transaction tx, String comment) throws NamingException {
+
+		final NamingEnumeration<NameClassPair> children = ctx.list(targetName);
+		try {
+			while (children.hasMore()) {
+				final NameClassPair child = children.next();
+				targetName.add(child.getName());
+				deleteRecursively(ctx, targetName, tx, "delete recursively");
+				targetName.remove(targetName.size() - 1);
+			}
+		} finally {
+			children.close();
+		}
+
+		DiropLogger.LOG.logDelete(targetName, comment);
+		try {
+			ctx.destroySubcontext(targetName);
+		} catch (final Exception e) {
+		}
+
+	}
+
+	/**
+	 * 
+	 */
+	protected void initPostLoad() {
+		for (final AttributeMapping am : attributes)
+			am.initPostLoad();
+	}
+
+	/**
+	 * @param mapping
+	 */
+	void addReferrer(AttributeMapping mapping) {
+		referrers.add(mapping);
+	}
+
+	public AttributeMapping getDNAttribute() {
+		return dnAttribute;
+	}
+
+	public void setDNAttribute(AttributeMapping dnAttribute) {
+		if (!dnAttribute.getFieldType().equals(String.class))
+			throw new IllegalArgumentException(
+					"The DN Attribute must be of type string");
+
+		this.dnAttribute = dnAttribute;
+		dnAttribute.setTypeMapping(this);
+	}
+
+	/**
+	 * @param o
+	 * @return
+	 * @throws DirectoryException
+	 */
+	String getDN(Object o) throws DirectoryException {
+		return (String) dnAttribute.getValue(o);
+	}
+
+	public String[] getObjectClasses() {
+		return objectClasses;
+	}
+
+	/**
+	 * Refresh the given object's state from the directory.
+	 * 
+	 * @param o the object to refresh.
+	 * @param tx the current transaction
+	 * @throws DirectoryException
+	 */
+	public void refresh(Object o, Transaction tx) throws DirectoryException {
+		try {
+			final DirContext ctx = tx.getContext(directoryFacade);
+			final String dn = getDN(o);
+			try {
+				final Name targetName = directoryFacade.makeRelativeName(dn);
+
+				if (logger.isDebugEnabled())
+					logger.debug("refreshing object of " + modelClass + " for dn: " + dn);
+
+				DiropLogger.LOG.logGetAttributes(dn, null, "refresh object");
+
+				final Attributes a = ctx.getAttributes(targetName);
+				hydrateInstance(a, o, tx);
+
+				for (final AttributeMapping am : attributes)
+					am.cascadePostLoad(o, tx);
+
+				// update object in cache, no matter what
+				final Name absoluteName = directoryFacade.makeAbsoluteName(dn);
+				tx.putCacheEntry(this, absoluteName, o, a);
+
+			} catch (final NameNotFoundException n) {
+				throw new DirectoryException("Can't refresh " + dn
+						+ ": object doesn't exist (any longer?)");
+			}
+		} catch (final DirectoryException e) {
+			throw e;
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't refresh object", e);
+		}
+	}
+
+	public void setScope(String scope) {
+		this.defaultScope = SearchScope.valueOf(scope);
+	}
+
+	public void setScope(SearchScope scope) {
+		this.defaultScope = scope;
+	}
+
+	/*
+	 * @see java.lang.Object#clone()
+	 */
+	@Override
+	protected TypeMapping clone() throws CloneNotSupportedException {
+		final TypeMapping clone = (TypeMapping) super.clone();
+
+		clone.referrers = new ArrayList<AttributeMapping>();
+		clone.attributes = new ArrayList<AttributeMapping>();
+
+		for (final AttributeMapping am : attributes) {
+			final AttributeMapping clonedAM = am.clone();
+			clone.add(clonedAM);
+		}
+
+		return clone;
+	}
+
+	public String getSearchFilter() {
+		return searchFilter;
+	}
+
+	public void setObjectClasses(String[] objectClasses) {
+		this.objectClasses = objectClasses;
+	}
+
+	public String getKeyClass() {
+		return keyClass;
+	}
+
+	void setDirectoryFacade(DirectoryFacade lcd) {
+		this.directoryFacade = lcd;
+	}
+
+	DirectoryFacade getDirectoryFacade() {
+		return directoryFacade;
+	}
+
+	public boolean matchesKeyClasses(Attribute objectClasses)
+			throws NamingException {
+		if (null == keyClass)
+			return false;
+
+		for (final NamingEnumeration<String> ne = (NamingEnumeration<String>) objectClasses
+				.getAll(); ne.hasMore();)
+			if (ne.next().equalsIgnoreCase(keyClass))
+				return true;
+
+		return false;
+	}
+
+	public Name getDefaultBaseName() throws InvalidNameException, NamingException {
+		if (null == defaultBaseName) {
+			final Name baseDNName = (Name) directoryFacade.getBaseDNName().clone();
+			defaultBaseName = baseDNName.add(getBaseRDN());
+		}
+
+		return defaultBaseName;
+	}
+
+	protected void collectRefererAttributes(
+			Set<ReferenceAttributeMapping> refererAttributes) {
+		for (final AttributeMapping am : attributes)
+			if (am instanceof ReferenceAttributeMapping)
+				refererAttributes.add((ReferenceAttributeMapping) am);
+	}
+
+	/**
+	 * Handle cases where the object's parent DN changed somewhere up the tree.
+	 * 
+	 * @param o the persistent object of which we need to up-date the name
+	 * @param oldName the old name prefix
+	 * @param newName the new name prefix
+	 * @param tx the current transaction
+	 * @throws DirectoryException
+	 * @throws NamingException
+	 */
+	void handleParentNameChange(Object o, String oldDN, String newDN,
+			Transaction tx) throws DirectoryException, NamingException {
+		final String dn = getDN(o);
+
+		// if the dn is null, the object is still transient and we don't have to
+		// worry
+		if (null != dn)
+			if (dn.endsWith(oldDN))
+				// update DN
+				setDN(dn.substring(0, dn.length() - oldDN.length()) + newDN, o);
+			else
+				logger.warn("Unexpected state during parent DN change: object's dn "
+						+ dn + " doesn't start with " + oldDN);
+
+		// perform cascading of stuff which has to be done after the
+		// new object has been saved.
+		for (final AttributeMapping attributeMapping : attributes)
+			attributeMapping.cascadeRDNChange(o, oldDN, newDN, tx);
+	}
+}

Propchange: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/TypeMapping.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/UnmodifiableHashtable.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/UnmodifiableHashtable.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/UnmodifiableHashtable.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/UnmodifiableHashtable.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,148 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * An unmodifiable view of a {@link Hashtable}, implemented by delegating all
+ * read-only methods to the table backing the view, but throwing an
+ * {@link UnsupportedOperationException} on all mutator methods.
+ * 
+ * This class sucks inherently, since {@link Hashtable} is a class instead of an
+ * interface. However, JNDI requires Hashtables for its environment properties
+ * instead of {@link Map}s. <em>sigh</em>.
+ * 
+ * @author levigo
+ * 
+ * @param <K> Key type
+ * @param <V> Value type
+ */
+class UnmodifiableHashtable<K, V> extends Hashtable<K, V> {
+	private static final long serialVersionUID = 1L;
+	private final Hashtable<K, V> delegate;
+
+	public UnmodifiableHashtable(Hashtable<K, V> m) {
+		this.delegate = m;
+	}
+
+	@Override
+	public synchronized void clear() {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public synchronized V put(K key, V value) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public synchronized void putAll(Map<? extends K, ? extends V> t) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public synchronized V remove(Object key) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public Set<Entry<K, V>> entrySet() {
+		return Collections.unmodifiableSet(delegate.entrySet());
+	}
+
+	@Override
+	public Set<K> keySet() {
+		return Collections.unmodifiableSet(delegate.keySet());
+	}
+
+	@Override
+	public Collection<V> values() {
+		return Collections.unmodifiableCollection(delegate.values());
+	}
+
+	@Override
+	public Object clone() {
+		return delegate.clone();
+	}
+
+	@Override
+	public boolean contains(Object value) {
+		return delegate.contains(value);
+	}
+
+	@Override
+	public boolean containsKey(Object key) {
+		return delegate.containsKey(key);
+	}
+
+	@Override
+	public boolean containsValue(Object value) {
+		return delegate.containsValue(value);
+	}
+
+	@Override
+	public Enumeration<V> elements() {
+		return delegate.elements();
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return delegate.equals(o);
+	}
+
+	@Override
+	public V get(Object key) {
+		return delegate.get(key);
+	}
+
+	@Override
+	public int hashCode() {
+		return delegate.hashCode();
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return delegate.isEmpty();
+	}
+
+	@Override
+	public Enumeration<K> keys() {
+		return delegate.keys();
+	}
+
+	@Override
+	public int size() {
+		return delegate.size();
+	}
+
+	@Override
+	public String toString() {
+		return delegate.toString();
+	}
+}
\ No newline at end of file

Propchange: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/UnmodifiableHashtable.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Util.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Util.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Util.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Util.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm;
+
+import javax.naming.Name;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.SearchControls;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A bunch of static utility methods.
+ * 
+ * @author levigo
+ */
+public class Util {
+	private static final Logger logger = LoggerFactory.getLogger(Util.class);
+
+	/**
+	 * Recursively delete a tree at/below a given {@link Name}.
+	 * 
+	 * @param ctx
+	 * @param targetName
+	 * @param tx
+	 * @throws NamingException
+	 */
+	public static void deleteRecursively(DirContext ctx, Name targetName)
+			throws NamingException {
+
+		final NamingEnumeration<NameClassPair> children = ctx.list(targetName);
+		try {
+			while (children.hasMore()) {
+				final NameClassPair child = children.next();
+				targetName.add(child.getName());
+				deleteRecursively(ctx, targetName);
+				targetName.remove(targetName.size() - 1);
+			}
+		} finally {
+			children.close();
+		}
+
+		if (logger.isDebugEnabled())
+			logger.debug("destroySubcontext: " + targetName);
+		try {
+			ctx.destroySubcontext(targetName);
+		} catch (final Exception e) {
+		}
+	}
+
+	/**
+	 * @param schema
+	 * @param sc
+	 * @throws NamingException
+	 */
+	public static boolean hasObjectClass(DirContext schema, String className)
+			throws NamingException {
+		final SearchControls sc = new SearchControls();
+		sc.setSearchScope(SearchControls.OBJECT_SCOPE);
+		try {
+			schema.list("ClassDefinition/" + className);
+			return true;
+		} catch (final NameNotFoundException e) {
+			return false;
+		}
+	}
+}

Propchange: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Util.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/CachingCallbackHandler.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/CachingCallbackHandler.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/CachingCallbackHandler.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/CachingCallbackHandler.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm.auth;
+
+import java.io.IOException;
+
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+/**
+ * @author levigo
+ */
+public interface CachingCallbackHandler extends CallbackHandler {
+	void purgeCache() throws IOException, UnsupportedCallbackException;
+}

Propchange: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/CachingCallbackHandler.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/UsernamePasswordHandler.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/UsernamePasswordHandler.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/UsernamePasswordHandler.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/UsernamePasswordHandler.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm.auth;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+public class UsernamePasswordHandler implements CallbackHandler {
+
+	private transient final String username;
+	private transient char[] password;
+	private transient final Object credential;
+
+	public UsernamePasswordHandler(String username, char[] password) {
+		super();
+		this.username = username;
+		this.password = password;
+		this.credential = password;
+	}
+
+	public UsernamePasswordHandler(String username, Object credential) {
+		super();
+		this.username = username;
+		this.credential = credential;
+	}
+
+	public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
+
+		for (int i = 0; i < callbacks.length; i++) {
+			Callback c = callbacks[i];
+			if (c instanceof NameCallback) {
+				NameCallback nc = (NameCallback) c;
+				nc.setName(username);
+			} else if (c instanceof PasswordCallback) {
+				PasswordCallback pc = (PasswordCallback) c;
+				if (password == null) {
+					if (credential != null) {
+						String tmp = credential.toString();
+						password = tmp.toCharArray();
+					}
+				}
+
+				pc.setPassword(password);
+			} else {
+				throw new UnsupportedCallbackException(callbacks[i],
+						"Unrecognized Callback");
+			}
+		}
+	}
+}

Propchange: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/auth/UsernamePasswordHandler.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/main/resources/org/apache/directory/odm/ldap-mapping.xml
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/resources/org/apache/directory/odm/ldap-mapping.xml?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/resources/org/apache/directory/odm/ldap-mapping.xml (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/resources/org/apache/directory/odm/ldap-mapping.xml Fri Dec 21 08:03:46 2007
@@ -0,0 +1,202 @@
+<?xml version="1.0"?>
+<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Object Mapping DTD Version 1.0//EN" 
+  "http://castor.exolab.org/mapping.dtd">
+<mapping>
+  <class name="org.apache.directory.odm.Mapping" verify-constructable="false">
+    <map-to xml="mapping" />
+    <field name="name" set-method="setName" get-method="getName">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="classes" type="org.apache.directory.odm.TypeMapping" collection="arraylist" set-method="add">
+      <bind-xml auto-naming="deriveByClass" node="element" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.TypeMapping" verify-constructable="false">
+    <map-to xml="class" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="baseDN" required="true" set-method="%2" type="string">
+      <bind-xml name="base-rdn" node="attribute" />
+    </field>
+    <field name="searchFilter" required="true" set-method="%3" type="string">
+      <bind-xml name="filter" node="attribute" />
+    </field>
+    <field name="scope" required="true" type="string" set-method="setScope">
+      <bind-xml name="scope" node="attribute" />
+    </field>
+    <field name="objectClasses" required="true" set-method="%4" type="string">
+      <bind-xml name="object-classes" node="attribute" />
+    </field>
+    <field name="keyClass" required="true" set-method="%5" type="string">
+      <bind-xml name="key-class" node="attribute" />
+    </field>
+    <field name="DNAttribute" required="true" type="org.apache.directory.odm.AttributeMapping"
+      set-method="setDNAttribute">
+      <bind-xml name="dn-attribute" node="element" />
+    </field>
+    <field name="RDNAttribute" required="true" type="org.apache.directory.odm.RDNAttributeMapping"
+      set-method="setRDNAttribute">
+      <bind-xml name="rdn-attribute" node="element" />
+    </field>
+    <field name="attributes" type="org.apache.directory.odm.AttributeMapping" collection="arraylist" set-method="add">
+      <bind-xml name="attribute" node="element" />
+    </field>
+    <field name="manyToMany" type="org.apache.directory.odm.ManyToManyMapping" collection="arraylist" set-method="add">
+      <bind-xml name="many-to-many" node="element" />
+    </field>
+    <field name="manyToOne" type="org.apache.directory.odm.ManyToOneMapping" collection="arraylist" set-method="add">
+      <bind-xml name="many-to-one" node="element" />
+    </field>
+    <field name="children" type="org.apache.directory.odm.ChildMapping" collection="arraylist" set-method="add">
+      <bind-xml name="child" node="element" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.GroupMapping" verify-constructable="false">
+    <map-to xml="group" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="baseRDN" required="true" set-method="%2" type="string">
+      <bind-xml name="base-rdn" node="attribute" />
+    </field>
+    <field name="searchFilter" required="true" set-method="%3" type="string">
+      <bind-xml name="filter" node="attribute" />
+    </field>
+    <field name="objectClasses" required="true" set-method="%4" type="string">
+      <bind-xml name="object-classes" node="attribute" />
+    </field>
+    <field name="keyClass" required="true" set-method="%5" type="string">
+      <bind-xml name="key-class" node="attribute" />
+    </field>  
+    <field name="memberAttribute" required="true" set-method="%6" type="string">
+      <bind-xml name="member-attribute" node="attribute" />
+    </field>  
+    <field name="DNAttribute" required="true" type="org.apache.directory.odm.AttributeMapping"
+      set-method="setDNAttribute">
+      <bind-xml name="dn-attribute" node="element" />
+    </field>
+    <field name="RDNAttribute" required="true" type="org.apache.directory.odm.RDNAttributeMapping"
+      set-method="setRDNAttribute">
+      <bind-xml name="rdn-attribute" node="element" />
+    </field>
+    <field name="members" type="org.apache.directory.odm.OneToManyMapping" collection="arraylist"
+      set-method="addMembers">
+      <bind-xml name="one-to-many" node="element" />
+    </field>
+    <field name="attributes" type="org.apache.directory.odm.AttributeMapping" collection="arraylist" set-method="add">
+      <bind-xml name="attribute" node="element" />
+    </field>
+    <field name="manyToMany" type="org.apache.directory.odm.ManyToManyMapping" collection="arraylist" set-method="add">
+      <bind-xml name="many-to-many" node="element" />
+    </field>
+    <field name="manyToOne" type="org.apache.directory.odm.ManyToOneMapping" collection="arraylist" set-method="add">
+      <bind-xml name="many-to-one" node="element" />
+    </field>
+    <field name="children" type="org.apache.directory.odm.ChildMapping" collection="arraylist" set-method="add">
+      <bind-xml name="child" node="element" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.RDNAttributeMapping" verify-constructable="false">
+    <map-to xml="rdn-attribute" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="getMethod" required="false" set-method="setGetMethod" type="string">
+      <bind-xml name="get-method" node="attribute" />
+    </field>
+    <field name="setMethod" required="true" set-method="setSetMethod" type="string">
+      <bind-xml name="set-method" node="attribute" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.AttributeMapping" verify-constructable="false">
+    <map-to xml="attribute" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="type" required="true" set-method="%2" type="string">
+      <bind-xml name="type" node="attribute" />
+    </field>
+    <field name="getMethod" required="false" set-method="setGetMethod" type="string">
+      <bind-xml name="get-method" node="attribute" />
+    </field>
+    <field name="setMethod" required="true" set-method="setSetMethod" type="string">
+      <bind-xml name="set-method" node="attribute" />
+    </field>
+    <field name="cardinality" required="false" set-method="setCardinality" type="string">
+      <bind-xml name="cardinality" node="attribute" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.ManyToOneMapping" verify-constructable="false">
+    <map-to xml="many-to-one" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="type" required="true" set-method="%2" type="string">
+      <bind-xml name="type" node="attribute" />
+    </field>
+    <field name="getMethod" required="true" set-method="setGetMethod" type="string">
+      <bind-xml name="get-method" node="attribute" />
+    </field>
+    <field name="setMethod" required="true" set-method="setSetMethod" type="string">
+      <bind-xml name="set-method" node="attribute" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.OneToManyMapping" verify-constructable="false">
+    <map-to xml="one-to-many" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="type" required="true" set-method="%2" type="string">
+      <bind-xml name="type" node="attribute" />
+    </field>
+    <field name="getMethod" required="true" set-method="setGetMethod" type="string">
+      <bind-xml name="get-method" node="attribute" />
+    </field>
+    <field name="setMethod" required="true" set-method="setSetMethod" type="string">
+      <bind-xml name="set-method" node="attribute" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.ManyToManyMapping" verify-constructable="false">
+    <map-to xml="many-to-many" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="type" required="true" set-method="%2" type="string">
+      <bind-xml name="type" node="attribute" />
+    </field>
+    <field name="getMethod" required="true" set-method="setGetMethod" type="string">
+      <bind-xml name="get-method" node="attribute" />
+    </field>
+    <field name="setMethod" required="true" set-method="setSetMethod" type="string">
+      <bind-xml name="set-method" node="attribute" />
+    </field>
+    <field name="memberField" required="true" set-method="setMemberField" type="string">
+      <bind-xml name="member-field" node="attribute" />
+    </field>
+    <field name="filter" required="false" set-method="setFilter" type="string">
+      <bind-xml name="filter" node="attribute" />
+    </field>
+  </class>
+  <class name="org.apache.directory.odm.ChildMapping" verify-constructable="false">
+    <map-to xml="child" />
+    <field name="name" required="true" set-method="%1" type="string">
+      <bind-xml name="name" node="attribute" />
+    </field>
+    <field name="type" required="true" set-method="%2" type="string">
+      <bind-xml name="type" node="attribute" />
+    </field>
+    <field name="getMethod" required="false" set-method="setGetMethod" type="string">
+      <bind-xml name="get-method" node="attribute" />
+    </field>
+    <field name="setMethod" required="false" set-method="setSetMethod" type="string">
+      <bind-xml name="set-method" node="attribute" />
+    </field>
+    <field name="filter" required="false" set-method="setFilter" type="string">
+      <bind-xml name="filter" node="attribute" />
+    </field>
+    <field name="cardinality" required="false" set-method="setCardinality" type="string">
+      <bind-xml name="cardinality" node="attribute" />
+    </field>
+  </class>
+</mapping>
\ No newline at end of file

Propchange: directory/sandbox/hennejg/odm/trunk/src/main/resources/org/apache/directory/odm/ldap-mapping.xml
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/AbstractEmbeddedDirectoryTest.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/AbstractEmbeddedDirectoryTest.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/AbstractEmbeddedDirectoryTest.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/AbstractEmbeddedDirectoryTest.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,268 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm.test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.Random;
+import java.util.Set;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+import org.apache.directory.odm.DirectoryException;
+import org.apache.directory.odm.DirectoryFacade;
+import org.apache.directory.odm.DiropLogger;
+import org.apache.directory.odm.LDAPConnectionDescriptor;
+import org.apache.directory.odm.Mapping;
+import org.apache.directory.odm.Util;
+import org.apache.directory.odm.LDAPConnectionDescriptor.ProviderType;
+import org.apache.directory.odm.test.model.OrganizationalUnit;
+import org.apache.directory.server.configuration.MutableServerStartupConfiguration;
+import org.apache.directory.server.core.configuration.Configuration;
+import org.apache.directory.server.core.configuration.MutablePartitionConfiguration;
+import org.apache.directory.server.core.configuration.PartitionConfiguration;
+import org.apache.directory.server.core.configuration.ShutdownConfiguration;
+import org.apache.directory.server.core.configuration.SyncConfiguration;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+public class AbstractEmbeddedDirectoryTest {
+
+	private static short ldapPort;
+	protected static String baseDN = "dc=test,dc=test";
+	protected static String envDN = "ou=test," + baseDN;
+
+	@BeforeClass
+	public static void setUp() throws Exception {
+		DiropLogger.LOG.enable(true, true);
+
+		ldapPort = getRandomNumber();
+
+		startService();
+	}
+
+	private static Hashtable createContextEnv() {
+		Hashtable env = new Properties();
+		env.put("java.naming.provider.url", "uid=admin,ou=system");
+		env.put("java.naming.factory.initial",
+				"org.apache.directory.server.jndi.ServerContextFactory");
+		env.put("java.naming.security.authentication", "simple");
+		env.put("java.naming.security.principal", "uid=admin,ou=system");
+		env.put("java.naming.security.credentials", "secret");
+		env.put(Configuration.JNDI_KEY, new SyncConfiguration());
+		return env;
+	}
+
+	private static void startService() throws Exception {
+		Hashtable env = createContextEnv();
+		MutableServerStartupConfiguration cfg = new MutableServerStartupConfiguration();
+		cfg.setAccessControlEnabled(false);
+		cfg.setAllowAnonymousAccess(true);
+		cfg.setEnableNetworking(true);
+		cfg.setLdapPort(ldapPort);
+		cfg.setEnableNtp(false);
+		cfg.setEnableKerberos(false);
+		cfg.setEnableChangePassword(false);
+		cfg.setWorkingDirectory(new File("unit-test-tmp"));
+//		cfg.setLdifDirectory(new File("unit-test-tmp"));
+
+		Set schemas = cfg.getBootstrapSchemas();
+		schemas.add(Class.forName(
+				"org.apache.directory.server.core.schema.bootstrap.NisSchema")
+				.newInstance());
+		cfg.setBootstrapSchemas(schemas);
+
+		Set pcfgs = new HashSet<PartitionConfiguration>();
+		pcfgs.add(createCustomPartition("dc=test,dc=test"));
+		cfg.setContextPartitionConfigurations(pcfgs);
+
+		env.putAll(cfg.toJndiEnvironment());
+
+		new InitialDirContext(env);
+	}
+
+	private static PartitionConfiguration createCustomPartition(String name)
+			throws NamingException {
+		BasicAttributes attrs;
+		Set indexedAttrs;
+		BasicAttribute attr;
+		MutablePartitionConfiguration pcfg = new MutablePartitionConfiguration();
+
+		// construct partition name from DN
+		String nameParts[] = name.split(",");
+		StringBuffer partitionName = new StringBuffer();
+		for (int i = 0; i < nameParts.length; i++) {
+			int idx = nameParts[i].indexOf('=');
+			if (i > 0)
+				partitionName.append('_');
+			partitionName.append(idx > 0
+					? nameParts[i].substring(idx + 1)
+					: nameParts[i]);
+		}
+
+		pcfg.setName(partitionName.toString());
+		pcfg.setSuffix(name);
+
+		indexedAttrs = new HashSet();
+		indexedAttrs.add("ou");
+		indexedAttrs.add("dc");
+		indexedAttrs.add("cn");
+		indexedAttrs.add("macAddress");
+		indexedAttrs.add("ipHostNumber");
+		indexedAttrs.add("objectClass");
+		pcfg.setIndexedAttributes(indexedAttrs);
+
+		attrs = new BasicAttributes(true);
+
+		attr = new BasicAttribute("objectClass");
+		attr.add("top");
+		attr.add("domain");
+		attr.add("extensibleObject");
+		attrs.put(attr);
+
+		attr = new BasicAttribute("dc");
+		attr.add(name);
+		attrs.put(attr);
+
+		pcfg.setContextEntry(attrs);
+
+		return pcfg;
+	}
+
+	@AfterClass
+	public static void cleanUp() throws NamingException {
+		ShutdownConfiguration cfg = new ShutdownConfiguration();
+		Hashtable env = createContextEnv();
+		env.putAll(cfg.toJndiEnvironment());
+		new InitialDirContext(env);
+
+		deleteRecursively(new File("unit-test-tmp"));
+	}
+
+	protected static LDAPConnectionDescriptor getConnectionDescriptor() {
+		final LDAPConnectionDescriptor lcd = new LDAPConnectionDescriptor();
+		lcd.setPortNumber(ldapPort);
+		lcd.setProviderType(ProviderType.SUN);
+
+		return lcd;
+	}
+
+	static void deleteRecursively(File file) {
+		if (!file.exists())
+			return;
+
+		if (file.isDirectory())
+			for (final File f : file.listFiles())
+				if (f.isDirectory())
+					deleteRecursively(f);
+				else
+					f.delete();
+
+		file.delete();
+	}
+
+	private static short getRandomNumber() {
+		final Random ran = new Random();
+		return (short) (11000 + ran.nextInt(999));
+	}
+
+	protected Mapping mapping;
+	protected LDAPConnectionDescriptor connectionDescriptor;
+
+	@Before
+	public void initEnv() throws Exception {
+		Mapping.disableCache = true;
+
+		connectionDescriptor = getConnectionDescriptor();
+		connectionDescriptor.setBaseDN(baseDN);
+
+		final DirectoryFacade facade = connectionDescriptor.createDirectoryFacade();
+		final DirContext ctx = facade.createDirContext();
+		try {
+			Util.deleteRecursively(ctx, facade.makeRelativeName(envDN));
+		} catch (final NameNotFoundException e) {
+			// ignore
+		} finally {
+			ctx.close();
+		}
+
+		mapping = Mapping.load(getClass().getResourceAsStream(
+				"/org/apache/odm/test/GENERIC_RFC.xml"));
+		mapping.initialize();
+
+		mapping.setConnectionDescriptor(connectionDescriptor);
+
+		final OrganizationalUnit ou = new OrganizationalUnit();
+		ou.setName("test");
+		ou.setDescription("openthinclient.org Console"); //$NON-NLS-1$
+		mapping.save(ou, "");
+
+		connectionDescriptor.setBaseDN(envDN);
+
+		// re-set mapping to the env DN
+		mapping.setConnectionDescriptor(connectionDescriptor);
+
+		final OrganizationalUnit users = new OrganizationalUnit();
+		users.setName("users");
+		mapping.save(users, "");
+
+		final OrganizationalUnit locations = new OrganizationalUnit();
+		locations.setName("locations");
+		mapping.save(locations, "");
+
+		final OrganizationalUnit usergroups = new OrganizationalUnit();
+		usergroups.setName("usergroups");
+		mapping.save(usergroups, "");
+
+		final OrganizationalUnit clients = new OrganizationalUnit();
+		clients.setName("clients");
+		mapping.save(clients, "");
+
+		final OrganizationalUnit hwtypes = new OrganizationalUnit();
+		hwtypes.setName("hwtypes");
+		mapping.save(hwtypes, "");
+	}
+
+	@After
+	public void destroyEnv() throws IOException, DirectoryException,
+			NamingException {
+		final LDAPConnectionDescriptor lcd = getConnectionDescriptor();
+		lcd.setBaseDN(baseDN);
+
+		final DirectoryFacade facade = lcd.createDirectoryFacade();
+		final DirContext ctx = facade.createDirContext();
+		try {
+			Util.deleteRecursively(ctx, facade.makeRelativeName(envDN));
+		} catch (final NameNotFoundException e) {
+			// ignore!
+		} finally {
+			ctx.close();
+		}
+	}
+}
\ No newline at end of file

Propchange: directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/AbstractEmbeddedDirectoryTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/TestBasicMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/TestBasicMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/TestBasicMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/TestBasicMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,503 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.directory.odm.test;
+
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.ldap.LdapContext;
+
+import org.apache.directory.odm.DirectoryException;
+import org.apache.directory.odm.DirectoryFacade;
+import org.apache.directory.odm.test.model.Client;
+import org.apache.directory.odm.test.model.HardwareType;
+import org.apache.directory.odm.test.model.Location;
+import org.apache.directory.odm.test.model.User;
+import org.apache.directory.odm.test.model.UserGroup;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestBasicMapping extends AbstractEmbeddedDirectoryTest {
+	private void createTestObjects() throws Exception {
+		final User u1 = new User();
+		u1.setName("jdoe");
+		u1.setGivenName("John");
+		u1.setSn("Doe");
+		mapping.save(u1);
+
+		final User u2 = new User();
+		u2.setName("hhirsch");
+		u2.setGivenName("Harry");
+		u2.setSn("Hirsch");
+		mapping.save(u2);
+
+		final UserGroup g1 = new UserGroup();
+		g1.setName("some users");
+		mapping.save(g1);
+
+		final UserGroup g2 = new UserGroup();
+		g2.setName("some users");
+		mapping.save(g2);
+
+		final Location l1 = new Location();
+		l1.setName("here");
+		mapping.save(l1);
+
+		final Location l2 = new Location();
+		l2.setName("there");
+		mapping.save(l2);
+	}
+
+	@Test
+	public void basicObjectProperties() throws DirectoryException {
+		User u = new User();
+
+		u.setName("someName");
+		u.setDescription("some description");
+		u.setGivenName("John");
+		u.setSn("Doe");
+		u.setUid(2345);
+		u.setUserPassword(new byte[]{1, 2, 3, 4, 5});
+
+		mapping.save(u);
+
+		// re-load the user
+		u = mapping.load(User.class, u.getDn());
+		Assert.assertNull("Location", u.getLocation());
+		Assert.assertEquals("Name", "someName", u.getName());
+		Assert.assertEquals("Description", "some description", u.getDescription());
+		Assert.assertEquals("GivenName", "John", u.getGivenName());
+		Assert.assertEquals("SN", "Doe", u.getSn());
+		Assert.assertEquals("uid", new Integer(2345), u.getUid());
+		Assert.assertArrayEquals("password", new byte[]{1, 2, 3, 4, 5}, u
+				.getUserPassword());
+
+		// check refresh as well
+		mapping.refresh(u);
+		Assert.assertNull("Location", u.getLocation());
+		Assert.assertEquals("Name", "someName", u.getName());
+		Assert.assertEquals("Description", "some description", u.getDescription());
+		Assert.assertEquals("GivenName", "John", u.getGivenName());
+		Assert.assertEquals("SN", "Doe", u.getSn());
+		Assert.assertEquals("uid", new Integer(2345), u.getUid());
+		Assert.assertArrayEquals("password", new byte[]{1, 2, 3, 4, 5}, u
+				.getUserPassword());
+	}
+
+	@Test
+	public void removeObject() throws Exception {
+		testUpdateProperty(); // will create tree of stuff
+
+		final Iterator<HardwareType> i = mapping.list(HardwareType.class)
+				.iterator();
+		final HardwareType t = i.next();
+
+		Assert.assertFalse("too many hardware types found", i.hasNext());
+
+		mapping.delete(t);
+
+		final DirectoryFacade f = connectionDescriptor.createDirectoryFacade();
+		final LdapContext ctx = f.createDirContext();
+		try {
+			ctx.getAttributes(f.makeRelativeName(t.getDn()));
+			Assert.fail("object has not been properly deleted");
+		} catch (final NameNotFoundException e) {
+			// expected
+		} finally {
+			ctx.close();
+		}
+	}
+
+	@Test
+	public void saveWithFixedDN() throws DirectoryException {
+		final User u = new User();
+
+		u.setDn("cn=foobar," + envDN); // doesn't exist!
+
+		try {
+			mapping.save(u);
+			Assert.fail("Expected exception not thrown");
+		} catch (final DirectoryException e) {
+			// expected
+		}
+	}
+
+	@Test
+	public void addManyToOne() throws DirectoryException {
+		Client c = new Client();
+		c.setName("someName");
+		c.setDescription("some description");
+
+		final Location l = new Location();
+		l.setName("whatever");
+
+		c.setLocation(l);
+
+		mapping.save(c);
+
+		// re-load the user
+		c = mapping.load(Client.class, c.getDn());
+		Assert.assertEquals("Location", l.getDn(), c.getLocation().getDn());
+
+		// check refresh as well
+		mapping.refresh(c);
+		Assert.assertEquals("Location", l.getDn(), c.getLocation().getDn());
+	}
+
+	@Test
+	public void addOneToMany() throws Exception {
+		createTestObjects();
+
+		final Set<User> users = mapping.list(User.class);
+		Assert.assertTrue("Doesn't have users", users.size() > 0);
+
+		final Set<UserGroup> userGroups = mapping.list(UserGroup.class);
+		Assert.assertTrue("Doesn't have groups", userGroups.size() > 0);
+
+		// add users to groups
+		for (final UserGroup group : userGroups) {
+			final Set<User> members = group.getMembers();
+			Assert.assertEquals("Group not empty", members.size(), 0);
+
+			members.addAll(users);
+
+			mapping.save(group);
+		}
+
+		for (UserGroup group : userGroups) {
+			// check with refresh
+			mapping.refresh(group);
+			Assert.assertTrue("Not all Objects were assigned (refresh)", group
+					.getMembers().containsAll(users));
+
+			// check with reload
+			group = mapping.load(UserGroup.class, group.getDn());
+			Assert.assertTrue("Not all Objects were assigned (reload)", group
+					.getMembers().containsAll(users));
+		}
+
+		// check inverse ends
+		for (User u : users) {
+			mapping.refresh(u);
+			Assert.assertTrue("User doesn't have all groups (refresh)", u
+					.getUserGroups().containsAll(userGroups));
+
+			u = mapping.load(User.class, u.getDn());
+			Assert.assertTrue("User doesn't have all groups (reload)", u
+					.getUserGroups().containsAll(userGroups));
+		}
+	}
+
+	@Test
+	public void removeAllFromOneToMany() throws Exception {
+		createTestObjects();
+
+		addOneToMany();
+
+		final Set<User> users = mapping.list(User.class);
+		final Set<UserGroup> userGroups = mapping.list(UserGroup.class);
+
+		for (final UserGroup group : userGroups) {
+			final Set<User> members = group.getMembers();
+			Assert.assertEquals("Group not full", members.size(), users.size());
+			members.clear();
+			mapping.save(group);
+		}
+
+		for (UserGroup group : userGroups) {
+			group = mapping.load(UserGroup.class, group.getDn(), true);
+			Assert.assertEquals("Not all Objects were removed", 0, group.getMembers()
+					.size());
+		}
+	}
+
+	@Test
+	public void removeOneFromOneToMany() throws Exception {
+		createTestObjects();
+
+		addOneToMany();
+
+		final Set<User> users = mapping.list(User.class);
+		final Set<UserGroup> userGroups = mapping.list(UserGroup.class);
+
+		final User toRemove = users.iterator().next();
+		users.remove(toRemove);
+
+		for (final UserGroup group : userGroups) {
+			final Set<User> members = group.getMembers();
+			Assert.assertEquals("Group not full", users.size() + 1, members.size());
+			members.remove(toRemove);
+			mapping.save(group);
+		}
+
+		for (UserGroup group : userGroups) {
+			group = mapping.load(UserGroup.class, group.getDn(), true);
+			Assert.assertEquals("Incorrect member count", users.size(), group
+					.getMembers().size());
+			Assert.assertTrue("Wrong members", group.getMembers().containsAll(users));
+		}
+	}
+
+	// This test tests a feature to be removed. See SUITE-69
+	@Test
+	public void addOneToManyInverse() throws Exception {
+		createTestObjects();
+
+		final User user = mapping.list(User.class).iterator().next();
+		final Set<UserGroup> userGroups = mapping.list(UserGroup.class);
+
+		Assert.assertEquals("User has groups", 0, user.getUserGroups().size());
+
+		user.setUserGroups(userGroups);
+
+		mapping.save(user);
+
+		mapping.refresh(user);
+
+		Assert.assertTrue("Not all Groups were assigned", user.getUserGroups()
+				.containsAll(userGroups));
+
+		for (final UserGroup userGroup : userGroups) {
+			mapping.refresh(userGroup);
+			Assert.assertTrue("Group doesn't contain user", userGroup.getMembers()
+					.contains(user));
+		}
+	}
+
+	// This test tests a feature to be removed. See SUITE-69
+	@Test
+	public void removeOneToManyInverse() throws Exception {
+		createTestObjects();
+
+		final User user = mapping.list(User.class).iterator().next();
+		final UserGroup group = mapping.list(UserGroup.class).iterator().next();
+
+		Assert.assertEquals("User has groups", 0, user.getUserGroups().size());
+		Assert.assertEquals("Group has users", 0, group.getMembers().size());
+
+		user.getUserGroups().add(group);
+		mapping.save(user);
+
+		mapping.refresh(group);
+		mapping.refresh(user);
+
+		Assert.assertTrue("User doesn't have group", user.getUserGroups().contains(
+				group));
+		Assert.assertTrue("Group doesn't have user", group.getMembers().contains(
+				user));
+
+		user.getUserGroups().remove(group);
+		mapping.save(user);
+
+		mapping.save(user);
+
+		mapping.refresh(group);
+		mapping.refresh(user);
+
+		Assert.assertFalse("User has group", user.getUserGroups().contains(group));
+		Assert.assertFalse("Group has user", group.getMembers().contains(user));
+	}
+
+	@SuppressWarnings("deprecation")
+	@Test
+	public void testUpdateProperty() throws Exception {
+		HardwareType type = new HardwareType();
+		type.setName("foo");
+		type.setValue("foo.bar", "foo");
+		type.setValue("foo.bar1", "foo");
+		type.setValue("foo.bar2", "foo");
+		type.setValue("foo.bar3", "foo");
+
+		mapping.save(type);
+
+		type = mapping.load(HardwareType.class, type.getDn());
+
+		Assert.assertEquals("Property", "foo", type.getValue("foo.bar"));
+		Assert.assertEquals("Property count", 4, type.getProperties().getMap()
+				.size());
+
+		type.setValue("foo.bar", "bar");
+
+		mapping.save(type);
+
+		type = mapping.load(HardwareType.class, type.getDn());
+
+		Assert.assertEquals("Property", "bar", type.getValue("foo.bar"));
+		Assert.assertEquals("Property count", 4, type.getProperties().getMap()
+				.size());
+	}
+
+	@SuppressWarnings("deprecation")
+	@Test
+	public void testAddProperty() throws Exception {
+		HardwareType type = new HardwareType();
+		type.setName("foo");
+		type.setValue("foo.bar", "foo");
+
+		mapping.save(type);
+
+		type = mapping.load(HardwareType.class, type.getDn());
+
+		Assert.assertEquals("Property", "foo", type.getValue("foo.bar"));
+		Assert.assertEquals("Property count", 1, type.getProperties().getMap()
+				.size());
+
+		type.setValue("foo.baz", "bar");
+
+		mapping.save(type);
+
+		type = mapping.load(HardwareType.class, type.getDn());
+
+		Assert.assertEquals("Property", "foo", type.getValue("foo.bar"));
+		Assert.assertEquals("Property", "bar", type.getValue("foo.baz"));
+		Assert.assertEquals("Property count", 2, type.getProperties().getMap()
+				.size());
+	}
+
+	@SuppressWarnings("deprecation")
+	@Test
+	public void testRemoveProperty() throws Exception {
+		HardwareType type = new HardwareType();
+		type.setName("foo");
+		type.setValue("foo.bar", "foo");
+		type.setValue("foo.baz", "bar");
+
+		mapping.save(type);
+
+		type = mapping.load(HardwareType.class, type.getDn());
+
+		Assert.assertEquals("Property", "foo", type.getValue("foo.bar"));
+		Assert.assertEquals("Property", "bar", type.getValue("foo.baz"));
+		Assert.assertEquals("Property count", 2, type.getProperties().getMap()
+				.size());
+
+		type.removeValue("foo.bar");
+
+		mapping.save(type);
+
+		type = mapping.load(HardwareType.class, type.getDn());
+
+		Assert.assertNull("Property", type.getValue("foo.bar"));
+		Assert.assertEquals("Property", "bar", type.getValue("foo.baz"));
+		Assert.assertEquals("Property count", 1, type.getProperties().getMap()
+				.size());
+	}
+
+	@Test
+	public void clearManyToOneReferencesOnDelete() throws DirectoryException {
+		Client c = new Client();
+		c.setName("someName");
+		c.setDescription("some description");
+
+		final Location l = new Location();
+		l.setName("whatever");
+
+		c.setLocation(l);
+
+		mapping.save(c);
+
+		mapping.delete(l);
+
+		// re-load the user
+		c = mapping.load(Client.class, c.getDn());
+		Assert.assertNull("Location still set", c.getLocation());
+
+		// check refresh as well
+		mapping.refresh(c);
+		Assert.assertNull("Location still set", c.getLocation());
+	}
+
+	@Test
+	public void clearOneToManyReferencesOnDelete() throws DirectoryException {
+		User u = new User();
+		u.setName("hhirsch");
+
+		UserGroup g = new UserGroup();
+		g.setName("some other group");
+
+		g.getMembers().add(u);
+
+		mapping.save(g);
+
+		u = mapping.load(User.class, u.getDn());
+		Assert.assertTrue("user is in group", u.getUserGroups().contains(g));
+
+		g = mapping.load(UserGroup.class, g.getDn());
+		Assert.assertTrue("group has user", g.getMembers().contains(u));
+
+		mapping.delete(u);
+
+		g = mapping.load(UserGroup.class, g.getDn());
+		Assert.assertEquals("group still has member", 0, g.getMembers().size());
+	}
+
+	@Test
+	public void updateManyToOneReferencesOnRename() throws DirectoryException {
+		Client c = new Client();
+		c.setName("someName");
+		c.setDescription("some description");
+
+		final Location l = new Location();
+		l.setName("whatever");
+
+		c.setLocation(l);
+
+		mapping.save(c);
+
+		c.setName("someOtherName");
+
+		mapping.save(c);
+
+		// re-load the user
+		c = mapping.load(Client.class, c.getDn());
+		Assert.assertEquals("Location no longer set", l, c.getLocation());
+
+		// check refresh as well
+		mapping.refresh(c);
+		Assert.assertEquals("Location no longer set", l, c.getLocation());
+	}
+
+	@Test
+	public void updateOneToManyReferencesOnRename() throws DirectoryException {
+		User u = new User();
+		u.setName("hhirsch");
+
+		UserGroup g = new UserGroup();
+		g.setName("some other group");
+
+		g.getMembers().add(u);
+
+		mapping.save(g);
+
+		u = mapping.load(User.class, u.getDn());
+		Assert.assertTrue("user is in group", u.getUserGroups().contains(g));
+
+		g = mapping.load(UserGroup.class, g.getDn());
+		Assert.assertTrue("group has user", g.getMembers().contains(u));
+
+		u.setName("hirschharry");
+		mapping.save(u);
+
+		g = mapping.load(UserGroup.class, g.getDn());
+		Assert.assertEquals("group no longer has member", 1, g.getMembers().size());
+		Assert.assertEquals("group no longer has member", u, g.getMembers()
+				.iterator().next());
+	}
+}
\ No newline at end of file

Propchange: directory/sandbox/hennejg/odm/trunk/src/test/java/org/apache/directory/odm/test/TestBasicMapping.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain