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 [2/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/ManyToManyMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToManyMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToManyMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToManyMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,282 @@
+/*******************************************************************************
+ * 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.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.NamingException;
+import javax.naming.directory.AttributeInUseException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The inverse side of a {@link GroupMapping}, i.e. the attribute pointing to
+ * the groups containing the object to which this attribute belongs.
+ * 
+ * @author levigo
+ */
+public class ManyToManyMapping extends AttributeMapping {
+	private static final Logger logger = LoggerFactory
+			.getLogger(ManyToManyMapping.class);
+
+	private String filter;
+	private String memberField;
+	private final Class peerType;
+
+	private GroupMapping peerMapping;
+
+	public ManyToManyMapping(String fieldName, String fieldType)
+			throws ClassNotFoundException {
+		super(fieldName, Set.class.getName());
+		this.peerType = Class.forName(fieldType);
+
+		if (!Object.class.isAssignableFrom(this.peerType))
+			throw new IllegalArgumentException("The field " + fieldName
+					+ " is not a subclass of Object");
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#valueFromAttributes(javax.naming.directory.Attribute)
+	 */
+	@Override
+	protected Object valueFromAttributes(Attributes attributes, final Object o,
+			final Transaction tx) throws NamingException, DirectoryException {
+		// make proxy for lazy loading
+		return Proxy.newProxyInstance(o.getClass().getClassLoader(),
+				new Class[]{Set.class}, new InvocationHandler() {
+					private Set realObjectSet;
+
+					public Object invoke(Object proxy, Method method, Object[] args)
+							throws Throwable {
+						if (null == realObjectSet) {
+							final String dn = type.getDN(o);
+
+							DiropLogger.LOG.logReadComment("LAZY LOAD: {0} containing {1}",
+									peerType.getSimpleName(), dn);
+
+							realObjectSet = loadObjectSet(dn);
+
+							// set real loaded object to original instance.
+							setValue(o, realObjectSet);
+						}
+						return method.invoke(realObjectSet, args);
+					};
+				});
+	}
+
+	/**
+	 * @param referencedDN
+	 * @param tx TODO
+	 * @return
+	 * @throws DirectoryException
+	 */
+	private Set loadObjectSet(String referencedDN) throws DirectoryException {
+		final Transaction tx = new Transaction(type.getMapping());
+		try {
+			referencedDN = peerMapping.getDirectoryFacade().fixNameCase(referencedDN);
+
+			return wrapValueSet(peerMapping.list(null != filter ? new Filter(filter,
+					referencedDN) : null, null, null, tx));
+		} catch (final NamingException e) {
+			throw new DirectoryException("Can't fix DN case for " + peerMapping);
+		} finally {
+			tx.commit();
+		}
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#dehydrate(org.openthinclient.common.directory.Object,
+	 *      javax.naming.directory.BasicAttributes)
+	 */
+	@Override
+	public Object dehydrate(Object o, BasicAttributes a)
+			throws DirectoryException {
+		// nothing to do
+		return null;
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#cascadePostSave(org.openthinclient.common.directory.Object)
+	 */
+	@Override
+	protected void cascadePostSave(Object o, Transaction tx, DirContext ctx)
+			throws DirectoryException {
+		// In preparation for SUITE-69: Check whether client code has modified the
+		// value set. Warn if a modification is detected.
+		// final Set newAssociations = (Set) getValue(o);
+		//
+		// if (null != newAssociations
+		// && !Proxy.isProxyClass(newAssociations.getClass())
+		// && newAssociations.size() > 0) {
+		// // Set is defined and not a proxy. Detect whether it is modifiable
+		// // by attempting to add a member to it.
+		// final Object something = newAssociations.iterator().next();
+		//
+		// try {
+		// // should be null-operation due to set-semantics
+		// newAssociations.add(something);
+		//
+		// // warn about transient change
+		// logger.warn("Changes to the field " + fieldName + " of type "
+		// + type.getMappedType() + " will not be persisted!");
+		// } catch (final UnsupportedOperationException e) {
+		// // expected/hoped for
+		// }
+		// }
+
+		// The following code has been commented out due to SUITE-69. It has,
+		// however, been left in place should there be the need to resurrect this
+		// functionality.
+
+		try {
+			// compare existing associations with the associations the saved object
+			// has
+			final Set newAssociations = (Set) getValue(o);
+
+			// if the associations aren't set at all, we don't care
+			if (null == newAssociations)
+				return;
+
+			if (null != newAssociations) {
+				// if the content is a proxy class, we don't have to save anything,
+				// since the association is unmodified.
+				if (Proxy.isProxyClass(newAssociations.getClass()))
+					return;
+
+				// save the association's members
+				for (final Object peer : newAssociations)
+					peerMapping.save(peer, null, tx);
+			}
+
+			// load existing associations
+			final String dn = peerMapping.getDirectoryFacade().fixNameCase(
+					type.getDN(o));
+			final Transaction nested = new Transaction(tx);
+			Set existing;
+			try {
+				existing = peerMapping.list(null != filter
+						? new Filter(filter, dn)
+						: null, null, null, nested);
+			} catch (final DirectoryException e) {
+				nested.rollback();
+				throw e;
+			} catch (final RuntimeException e) {
+				nested.rollback();
+				throw e;
+			} finally {
+				nested.commit();
+			}
+
+			final List missing = new LinkedList();
+			if (null != newAssociations)
+				missing.addAll(newAssociations);
+
+			for (final Iterator i = missing.iterator(); i.hasNext();)
+				if (existing.remove(i.next()))
+					i.remove();
+
+			// missing now has the missing ones, existing the ones to be removed
+			for (final Iterator i = existing.iterator(); i.hasNext();) {
+				final Object group = i.next();
+				if (logger.isDebugEnabled())
+					logger.debug("Remove: " + group);
+				peerMapping.removeMember(group, memberField, dn, tx);
+			}
+			for (final Iterator i = missing.iterator(); i.hasNext();)
+				try {
+					final Object group = i.next();
+					if (logger.isDebugEnabled())
+						logger.debug("Save: " + group);
+					if (!peerMapping.isInDirectory(group, memberField, dn, tx))
+						peerMapping.addMember(group, memberField, dn, tx);
+					else
+						logger.error("Object already exists !!!");
+				} catch (final AttributeInUseException a) {
+					logger.error("Object already exists !!!", a);
+				}
+
+		} catch (final DirectoryException e) {
+			throw e;
+		} catch (final Exception e) {
+			throw new DirectoryException("Can't update many-to-many association", e);
+		}
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#checkNull(javax.naming.directory.Attributes)
+	 */
+	@Override
+	protected boolean checkNull(Attributes a) {
+		return false;
+	}
+
+	public void setFilter(String filter) {
+		this.filter = filter;
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#initNewInstance(org.openthinclient.common.directory.Object)
+	 */
+	@Override
+	protected void initNewInstance(Object instance) throws DirectoryException {
+		setValue(instance, wrapValueSet(new HashSet()));
+	}
+
+	private Set wrapValueSet(Set s) {
+		// commented-out until SUITE-69 is implemented.
+		// return Collections.unmodifiableSet(s);
+
+		return s;
+	}
+
+	public void setMemberField(String memberField) {
+		this.memberField = memberField;
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#initPostLoad()
+	 */
+	@Override
+	protected void initPostLoad() {
+		super.initPostLoad();
+		final TypeMapping peer = type.getMapping().getMapping(peerType);
+		if (null == peer)
+			throw new IllegalStateException(this + ": no mapping for peer type "
+					+ peerType);
+
+		if (!(peer instanceof GroupMapping))
+			throw new IllegalStateException("many-to-many-mapping " + this
+					+ " needs a group as a partner, not a " + peer);
+
+		this.peerMapping = (GroupMapping) peer;
+
+		peer.addReferrer(this);
+	}
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToOneMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToOneMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToOneMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ManyToOneMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * 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.NameNotFoundException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author levigo
+ */
+public class ManyToOneMapping extends ReferenceAttributeMapping {
+	private static final Logger logger = LoggerFactory.getLogger(ManyToOneMapping.class);
+
+	private final Class refereeType;
+	private TypeMapping refereeMapping;
+
+	public ManyToOneMapping(String fieldName, String fieldType)
+			throws ClassNotFoundException {
+		super(fieldName, fieldType);
+		this.refereeType = Class.forName(fieldType);
+
+		if (!Object.class.isAssignableFrom(this.refereeType))
+			throw new IllegalArgumentException("The field " + fieldName
+					+ " is not a subclass of Object");
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#valueFromAttributes(javax.naming.directory.Attribute)
+	 */
+	@Override
+	protected Object valueFromAttributes(Attributes attributes, Object o,
+			Transaction tx) throws NamingException, DirectoryException {
+		final Attribute attribute = attributes.get(fieldName);
+		if (null != attribute) {
+			final String dn = attribute.get().toString();
+			try {
+				return type.getMapping() //
+						.getMapping(getFieldType(), dn) //
+						.load(dn, tx);
+			} catch (final NameNotFoundException e) {
+				logger.warn("Referenced object for many-to-one mapping not found: "
+						+ dn);
+			}
+		}
+
+		return null;
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#initPostLoad()
+	 */
+	@Override
+	protected void initPostLoad() {
+		super.initPostLoad();
+		final TypeMapping child = type.getMapping().getMapping(refereeType);
+		if (null == child)
+			throw new IllegalStateException(this + ": no mapping for peer type "
+					+ refereeType);
+
+		this.refereeMapping = child;
+
+		child.addReferrer(this);
+	}
+
+	/*
+	 * @see org.apache.directory.odm.AttributeMapping#getValue(java.lang.Object)
+	 */
+	@Override
+	protected Object getValue(Object o) throws DirectoryException {
+		final Object referenced = super.getValue(o);
+		if (null == referenced)
+			return null;
+		return refereeMapping.getDN(referenced);
+	}
+
+	/*
+	 * @see org.apache.directory.odm.AttributeMapping#cascadePreSave(java.lang.Object,
+	 *      org.apache.directory.odm.Transaction)
+	 */
+	@Override
+	protected void cascadePreSave(Object o, Transaction tx)
+			throws DirectoryException {
+		super.cascadePreSave(o, tx);
+		final Object referenced = super.getValue(o);
+		if (null != referenced)
+			refereeMapping.save(referenced, null, tx);
+	}
+
+	@Override
+	Cardinality getCardinality() {
+		return Cardinality.ZERO_OR_ONE;
+	}
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Mapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Mapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Mapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Mapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,817 @@
+/*******************************************************************************
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.Name;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.apache.directory.odm.TypeMapping.SearchScope;
+import org.exolab.castor.mapping.MappingException;
+import org.exolab.castor.xml.MarshalException;
+import org.exolab.castor.xml.Unmarshaller;
+import org.exolab.castor.xml.ValidationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.InputSource;
+
+/**
+ * @author levigo
+ */
+public class Mapping {
+	/**
+	 * For unit-test purposes only...
+	 */
+	public static boolean disableCache = false;
+
+	private static final Logger logger = LoggerFactory.getLogger(Mapping.class);
+
+	/**
+	 * Property key to be used when all directory operations should be forced into
+	 * a single-threaded access model. If this property is set to a non-<code>null</code>
+	 * value, all accesses are synchronized. This will limit the number of
+	 * parallel directory operations effected by the mapping to one.
+	 */
+	public static final String PROPERTY_FORCE_SINGLE_THREADED = "ldap.mapping.single-treaded";
+
+	/**
+	 * Load an LDAP Mapping
+	 * 
+	 * @param path
+	 * @return
+	 * @throws IOException
+	 * @throws MappingException
+	 * @throws MarshalException
+	 * @throws ValidationException
+	 * @throws MarshalException
+	 */
+	public static Mapping load(InputStream is) throws IOException,
+			MappingException, ValidationException, MarshalException {
+		// Create a Reader to the file to unmarshal from
+		final InputStreamReader reader = new InputStreamReader(is);
+
+		// Create a new Unmarshaller
+		final org.exolab.castor.mapping.Mapping m = new org.exolab.castor.mapping.Mapping();
+		m.loadMapping(new InputSource(Mapping.class
+				.getResourceAsStream("ldap-mapping.xml")));
+		final Unmarshaller unmarshaller = new Unmarshaller(m);
+
+		// Unmarshal the configuration object
+		final Mapping loadedMapping = (Mapping) unmarshaller.unmarshal(reader);
+
+		return loadedMapping;
+	}
+
+	/**
+	 * The default type mappers, i.e. the ones to be used, when no explicit target
+	 * directory is selected.
+	 */
+	private final Map<Class, TypeMapping> defaultMappers = new HashMap<Class, TypeMapping>();
+
+	private boolean initialized;
+
+	/**
+	 * All mappers mamaged by this mapping
+	 */
+	private final Set<TypeMapping> mappers = new HashSet<TypeMapping>();
+
+	/**
+	 * The mappers indexed by connection descriptor (i.e. target Directory Server)
+	 */
+	private final Map<DirectoryFacade, Set<TypeMapping>> mappersByDirectory = new HashMap<DirectoryFacade, Set<TypeMapping>>();
+
+	/**
+	 * The mappers indexed by mapped type
+	 */
+	private final Map<Class, Set<TypeMapping>> mappersByType = new HashMap<Class, Set<TypeMapping>>();
+
+	/**
+	 * The Mapping's name.
+	 */
+	private String name;
+
+	// FIXME: make this configurable
+	private final SecondLevelCache secondLevelCache = new EhCacheSecondLevelCache();
+
+	public Mapping() {
+
+	}
+
+	public Mapping(Mapping m) {
+		this();
+
+		for (final TypeMapping tm : m.mappers)
+			try {
+				this.add(tm.clone());
+			} catch (final CloneNotSupportedException e1) {
+				// should not happen. If it does, we're doomed anyway.
+				throw new RuntimeException(e1);
+			}
+
+		initialize();
+	}
+
+	/**
+	 * Add a type mapping.
+	 * 
+	 * @param typeMapping
+	 */
+	public void add(TypeMapping typeMapping) {
+		if (mappers.contains(typeMapping))
+			throw new IllegalArgumentException(
+					"The specified TypeMapping already contained in this mapping");
+
+		typeMapping.setMapping(this);
+
+		mappers.add(typeMapping);
+		defaultMappers.put(typeMapping.getMappedType(), typeMapping);
+
+		// maintain index by class
+		Set<TypeMapping> mappersForClass = mappersByType.get(typeMapping
+				.getMappedType());
+		if (null == mappersForClass) {
+			mappersForClass = new HashSet<TypeMapping>();
+			mappersByType.put(typeMapping.getMappedType(), mappersForClass);
+		}
+
+		mappersForClass.add(typeMapping);
+
+		// maintain index by directory
+		final DirectoryFacade lcd = typeMapping.getDirectoryFacade();
+		if (null != lcd) {
+			Set<TypeMapping> mappersForConnection = mappersByDirectory.get(lcd);
+			if (null == mappersForConnection) {
+				mappersForConnection = new HashSet<TypeMapping>();
+				mappersByDirectory.put(lcd, mappersForConnection);
+			}
+			mappersForConnection.add(typeMapping);
+		}
+	}
+
+	/**
+	 * Close this mapping. Closing a mapping currently has the sole effect of
+	 * purging the cache.
+	 */
+	public void close() {
+		try {
+			if (null != secondLevelCache)
+				secondLevelCache.clear();
+		} catch (final Exception e) {
+			logger.error("Can't purge cache", e);
+		}
+	}
+
+	/**
+	 * Create an object (new instance) of the given type and initialize the RDN
+	 * atttribute.
+	 * 
+	 * @param type
+	 * @return
+	 * @throws DirectoryException
+	 */
+	public <T> T create(Class<T> type) throws DirectoryException {
+		if (logger.isDebugEnabled())
+			logger.debug("create(): create=" + type);
+
+		final TypeMapping tm = defaultMappers.get(type);
+		if (null == tm)
+			throw new IllegalArgumentException("No mapping for class " + type);
+
+		return (T) tm.create();
+	}
+
+	/**
+	 * Remove the given object from the directory.
+	 * 
+	 * @param object
+	 * @throws DirectoryException
+	 */
+	public boolean delete(Object object) throws DirectoryException {
+		if (logger.isDebugEnabled())
+			logger.debug("delete(): type=" + object.getClass());
+
+		final TypeMapping tm = defaultMappers.get(object.getClass());
+		if (null == tm)
+			throw new IllegalArgumentException("No mapping for class "
+					+ object.getClass());
+
+		final Transaction tx = new Transaction(this);
+		try {
+			return tm.delete(object, tx);
+		} catch (final DirectoryException e) {
+			tx.rollback();
+			throw e;
+		} catch (final RuntimeException e) {
+			tx.rollback();
+			throw e;
+		} finally {
+			if (!tx.isClosed())
+				tx.commit();
+		}
+	}
+
+	/**
+	 * Get set of {@link TypeMapping}s managed by this Mapping.
+	 * 
+	 * @return
+	 */
+	Set<TypeMapping> getMappers() {
+		return mappers;
+	}
+
+	/**
+	 * Return the (default) TypeMapping for a given class.
+	 * 
+	 * @param c
+	 * @return
+	 */
+	TypeMapping getMapping(Class c) {
+		return defaultMappers.get(c);
+	}
+
+	/**
+	 * Find the TypeMapping to use for a given mapped type where the connection
+	 * descriptor is the same as the specified one.
+	 * 
+	 * @param type the mapped type
+	 * @param baseDN the base DN of the object to be handled or <code>null</code>
+	 *          if it is not (yet?) known.
+	 * 
+	 * @return
+	 * @throws NamingException
+	 */
+	TypeMapping getMapping(Class type, DirectoryFacade connectionDescriptor) {
+		final Set<TypeMapping> mappers = mappersByDirectory
+				.get(connectionDescriptor);
+		for (final TypeMapping tm : mappers)
+			if (tm.getMappedType().equals(type))
+				return tm;
+
+		throw new IllegalArgumentException(
+				"No mapping for the specified type and connection descriptor");
+	}
+
+	/**
+	 * Find the TypeMapping to use for a given mapped type. Refined by base DN if
+	 * appropriate/necessary.
+	 * 
+	 * @param type the mapped type
+	 * @param baseDN the base DN of the object to be handled or <code>null</code>
+	 *          if it is not (yet?) known.
+	 * 
+	 * @return
+	 * @throws NamingException
+	 */
+	TypeMapping getMapping(Class type, String baseDN) throws NamingException {
+		// no base DN -> use default mapping
+		if (null == baseDN)
+			return defaultMappers.get(type);
+
+		// try to find suitable mapping, assuming that the base DN is absolute
+		final Set<TypeMapping> mappersForClass = mappersByType.get(type);
+		for (final TypeMapping tm : mappersForClass)
+			if (tm.getDirectoryFacade().contains(
+					tm.getDirectoryFacade().getNameParser().parse(baseDN)))
+				return tm;
+
+		// no cigar? fall back to default
+		return defaultMappers.get(type);
+	}
+
+	/**
+	 * Return the mapping for the object at a given DN. In order to determine the
+	 * mapping, the object's objectClasses need to be loaded.
+	 * 
+	 * @param dn
+	 * @param objectClasses
+	 * @param tx current transaction
+	 * @return
+	 * @throws DirectoryException
+	 * @throws NamingException
+	 */
+	TypeMapping getMapping(String dn, Transaction tx) throws DirectoryException,
+			NamingException {
+		for (final Map.Entry<DirectoryFacade, Set<TypeMapping>> e : mappersByDirectory
+				.entrySet()) {
+			// check whether the directory contains the dn
+			final DirectoryFacade df = e.getKey();
+			final Name parsedDN = df.getNameParser().parse(dn);
+			if (df.contains(parsedDN)) {
+				// load the object and determine the object class
+				final DirContext ctx = tx.getContext(df);
+				final String[] attributes = {"objectClass"};
+
+				DiropLogger.LOG.logGetAttributes(dn, attributes, "determining mapping");
+
+				final Attributes a = ctx.getAttributes(df.makeRelativeName(dn),
+						attributes);
+				final Attribute objectClasses = a.get("objectClass");
+
+				final Set<TypeMapping> mappings = e.getValue();
+
+				final TypeMapping match = getMapping(parsedDN, objectClasses, mappings);
+				if (null != match)
+					return match;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Find the best TypeMapping for an object described by its DN an object
+	 * classes from a set of mappings.
+	 * 
+	 * @param parsedDN
+	 * @param objectClasses
+	 * @param mappings
+	 * @return
+	 * @throws NamingException
+	 * @throws InvalidNameException
+	 */
+	private TypeMapping getMapping(final Name parsedDN,
+			final Attribute objectClasses, Set<TypeMapping> mappings)
+			throws NamingException, InvalidNameException {
+		// build list of mapping candidates. There may be more than one!
+		final List<TypeMapping> candidates = new ArrayList<TypeMapping>();
+		for (final TypeMapping tm : mappings)
+			if (tm.matchesKeyClasses(objectClasses))
+				candidates.add(tm);
+
+		// if there is only one match, return it
+		if (candidates.size() == 1)
+			return candidates.get(0);
+
+		// if more than one match, select best one by base RDN
+		for (final TypeMapping tm : candidates)
+			if (tm.getBaseRDN() != null)
+				if (parsedDN.startsWith(tm.getDefaultBaseName()))
+					return tm;
+
+		// no "best" match -> just use first one
+		if (candidates.size() > 0)
+			return candidates.get(0);
+
+		return null;
+	}
+
+	/**
+	 * Get a map of {@link TypeMapping}s by mapped class.
+	 * 
+	 * @return
+	 */
+	public Map<Class, TypeMapping> getTypes() {
+		return Collections.unmodifiableMap(defaultMappers);
+	}
+
+	/**
+	 * Initialize this Mapping. Used after unmarshalling it from XML.
+	 */
+	public void initialize() {
+		if (initialized)
+			return;
+
+		for (final TypeMapping m : defaultMappers.values())
+			m.initPostLoad();
+
+		initialized = true;
+
+		if (logger.isDebugEnabled())
+			logger.debug("LDAP mapping initialized");
+	}
+
+	/**
+	 * List all objects of the given type located at the default base DN for the
+	 * given type.
+	 * 
+	 * @param type
+	 * @return
+	 * @throws DirectoryException
+	 */
+	public <T> Set<T> list(Class<T> type) throws DirectoryException {
+		if (!initialized)
+			throw new DirectoryException(
+					"Mapping is not yet initialized - call initialize() first");
+
+		return list(type, null, null, null);
+	}
+
+	/**
+	 * List objects of the given type at or below the given search base using the
+	 * given context.
+	 * 
+	 * @param <T>
+	 * @param type
+	 * @param filter
+	 * @param baseDN
+	 * @param scope
+	 * @return
+	 * @throws DirectoryException
+	 */
+	@SuppressWarnings("cast")
+	public <T> Set<T> list(Class<T> type, Filter filter, String baseDN,
+			SearchScope scope) throws DirectoryException {
+		if (!initialized)
+			throw new DirectoryException(
+					"Mapping is not yet initialized - call initialize() first");
+
+		if (logger.isDebugEnabled())
+			logger.debug("list(): type=" + type + ", filter=" + filter
+					+ ", searchBase=" + baseDN);
+
+		// get mapper. try to find one for the specified search base first
+		TypeMapping tm;
+		try {
+			tm = getMapping(type, baseDN);
+		} catch (final NamingException e) {
+			throw new DirectoryException(
+					"Can't determine TypeMapping for this type and search base", e);
+		}
+
+		// fall back to default mapping if not found
+		if (null == tm)
+			tm = defaultMappers.get(type);
+
+		if (null == tm)
+			throw new IllegalArgumentException("No mapping for class " + type);
+
+		final Transaction tx = new Transaction(this);
+		try {
+			return (Set<T>) tm.list(filter, baseDN, scope, tx);
+		} catch (final DirectoryException e) {
+			tx.rollback();
+			throw e;
+		} catch (final RuntimeException e) {
+			tx.rollback();
+			throw e;
+		} finally {
+			if (!tx.isClosed())
+				tx.commit();
+		}
+	}
+
+	/**
+	 * Load an object of the given type from the given dn.
+	 * 
+	 * @param type the (expected) type
+	 * @param dn the object's dn
+	 * @return
+	 * @throws DirectoryException
+	 */
+	public <T> T load(Class<T> type, String dn) throws DirectoryException {
+		return load(type, dn, false);
+	}
+
+	/**
+	 * Load an object of the given type from the given dn.
+	 * 
+	 * @param type the (expected) type
+	 * @param dn the object's dn
+	 * @param noCache if <code>true</code> the cache will not be consulted for
+	 *          this object.
+	 * @return
+	 * @throws DirectoryException
+	 */
+	public <T> T load(Class<T> type, String dn, boolean noCache)
+			throws DirectoryException {
+		if (!initialized)
+			throw new DirectoryException(
+					"Mapping is not yet initialized - call initialize() first");
+
+		if (logger.isDebugEnabled())
+			logger.debug("load(): type=" + type + ", dn=" + dn);
+
+		TypeMapping tm;
+		try {
+			tm = getMapping(type, dn);
+		} catch (final NamingException e) {
+			throw new DirectoryException(
+					"Can't determine TypeMapping for this type and DN", e);
+		}
+
+		if (null == tm)
+			throw new IllegalArgumentException("No mapping for class " + type);
+
+		final Transaction tx = new Transaction(this, noCache);
+		try {
+			return (T) tm.load(dn, tx);
+		} catch (final DirectoryException e) {
+			tx.rollback();
+			throw e;
+		} catch (final RuntimeException e) {
+			tx.rollback();
+			throw e;
+		} finally {
+			if (!tx.isClosed())
+				tx.commit();
+		}
+	}
+
+	/**
+	 * Refresh the given object's state from the directory. The object must
+	 * already have a DN for this operation to succeed. This operations always
+	 * by-passes the cache, making sure that the object's state after the refresh
+	 * is consistent with the directory.
+	 * 
+	 * @param a
+	 * @throws DirectoryException
+	 */
+	public void refresh(Object o) throws DirectoryException {
+		if (!initialized)
+			throw new DirectoryException(
+					"Mapping is not yet initialized - call initialize() first");
+
+		if (logger.isDebugEnabled())
+			logger.debug("refresh(): object=" + o);
+
+		final TypeMapping tm = defaultMappers.get(o.getClass());
+		if (null == tm)
+			throw new IllegalArgumentException("No mapping for class " + o.getClass());
+
+		final Transaction tx = new Transaction(this, true);
+		try {
+			tm.refresh(o, tx);
+		} catch (final DirectoryException e) {
+			tx.rollback();
+			throw e;
+		} catch (final RuntimeException e) {
+			tx.rollback();
+			throw e;
+		} finally {
+			if (!tx.isClosed())
+				tx.commit();
+		}
+	}
+
+	/**
+	 * Remove the given {@link TypeMapping}.
+	 * 
+	 * @param tm
+	 */
+	public void remove(TypeMapping tm) {
+		if (!mappers.remove(tm))
+			return;
+
+		defaultMappers.remove(tm.getMappedType());
+
+		// maintain index by type
+		final Set<TypeMapping> forType = mappersByType.get(tm.getMappedType());
+		if (null != forType) {
+			forType.remove(tm);
+			if (forType.isEmpty())
+				mappersByType.remove(tm.getMappedType());
+		}
+
+		// maintain index by connection
+		final Set<TypeMapping> forConnection = mappersByDirectory.get(tm
+				.getDirectoryFacade());
+		if (null != forConnection) {
+			forConnection.remove(tm);
+			if (forConnection.isEmpty())
+				mappersByDirectory.remove(tm.getDirectoryFacade());
+		}
+	}
+
+	/**
+	 * Save the given object to the default base DN appropriate for the given
+	 * object type.
+	 * 
+	 * @param o the object to be saved
+	 * @throws DirectoryException
+	 */
+	public void save(Object o) throws DirectoryException {
+		save(o, null);
+	}
+
+	/**
+	 * Save the given object to the given base DN. The object DN will be made up
+	 * from the base DN and the object's RDN. If the object was already persistent
+	 * and is therefore only updated, specifying the base DN will have no effect.
+	 * 
+	 * @param o the object to be saved
+	 * @param baseDN the base DN at which to save the object
+	 * @throws DirectoryException
+	 */
+	public void save(Object o, String baseDN) throws DirectoryException {
+		if (!initialized)
+			throw new DirectoryException(
+					"Mapping is not yet initialized - call initialize() first");
+
+		if (logger.isDebugEnabled())
+			logger.debug("save(): object=" + o + ", baseDN=" + baseDN);
+		final TypeMapping tm = defaultMappers.get(o.getClass());
+		if (null == tm)
+			throw new IllegalArgumentException("No mapping for class " + o.getClass());
+
+		final Transaction tx = new Transaction(this);
+		try {
+			tm.save(o, baseDN, tx);
+		} catch (final DirectoryException e) {
+			tx.rollback();
+			throw e;
+		} catch (final RuntimeException e) {
+			tx.rollback();
+			throw e;
+		} finally {
+			if (!tx.isClosed())
+				tx.commit();
+		}
+	}
+
+	/**
+	 * Set the directory connection to be used.
+	 * 
+	 * @param lcd
+	 * 
+	 * @see #setDirectoryFacade(DirectoryFacade)
+	 */
+	public void setConnectionDescriptor(LDAPConnectionDescriptor lcd) {
+		setDirectoryFacade(lcd.createDirectoryFacade());
+	}
+
+	/**
+	 * Set the {@link DirectoryFacade} to be used for all accesses to the
+	 * directory. This method may be used instead of
+	 * {@link #setConnectionDescriptor(LDAPConnectionDescriptor)} if a
+	 * {@link DirectoryFacade} has already been obtained otherwise.
+	 * 
+	 * @param env
+	 * 
+	 * @see #setConnectionDescriptor(LDAPConnectionDescriptor)
+	 */
+	public void setDirectoryFacade(DirectoryFacade lcd) {
+		// iterate over a copy to prevent a CME
+		for (final TypeMapping tm : new ArrayList<TypeMapping>(mappers)) {
+			// remove and add to preserve mapping indexes
+			remove(tm);
+			tm.setDirectoryFacade(lcd);
+			add(tm);
+		}
+	}
+
+	/**
+	 * Clear/update all references to the specified dn. This is usually used in
+	 * response to an object deletion/rename.
+	 * 
+	 * @param tx
+	 * @param oldDN the name of the existing object being referred to
+	 * @param newDN the new name of the object, or <code>null</code> if the
+	 *          object has been deleted.
+	 * @throws DirectoryException
+	 * @throws NamingException
+	 */
+	void updateReferences(Transaction tx, String oldDN, String newDN)
+			throws DirectoryException, NamingException {
+		// iterate over target directories, so that we can query the referrers
+		// efficiently using just one query per directory.
+		for (final Map.Entry<DirectoryFacade, Set<TypeMapping>> e : mappersByDirectory
+				.entrySet()) {
+			final Set<TypeMapping> mappers = e.getValue();
+			final DirectoryFacade directory = e.getKey();
+
+			// Build list of referrer attributes.
+			final Set<ReferenceAttributeMapping> refererAttributes = new HashSet<ReferenceAttributeMapping>();
+			for (final TypeMapping m : mappers)
+				m.collectRefererAttributes(refererAttributes);
+
+			// compress references into set of attribute names and a set of type
+			// mappings (not all types have references at all!)
+			final Set<String> attributeNames = new HashSet<String>();
+			final Set<TypeMapping> effectiveMappers = new HashSet<TypeMapping>();
+			for (final ReferenceAttributeMapping ra : refererAttributes) {
+				attributeNames.add(ra.getFieldName());
+				effectiveMappers.add(ra.getTypeMapping());
+			}
+
+			// build filter expression
+			final DirContext ctx = tx.getContext(directory);
+			final StringBuilder sb = new StringBuilder("(|");
+			for (final String name : attributeNames)
+				sb.append("(").append(name).append("=").append(oldDN).append(")");
+			sb.append(")");
+
+			// we query by referrer attribute name only. Theoretically, we would also
+			// need to use the object class in the query, but we can probably get
+			// away with this simplification in all practical cases.
+			final SearchControls sc = new SearchControls();
+			sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
+			sc.setReturningAttributes(new String[refererAttributes.size()]);
+			sc.setDerefLinkFlag(false);
+
+			final String filter = sb.toString();
+
+			DiropLogger.LOG.logSearch("", filter, null, sc, "searching references");
+
+			// issue query to find referencing objects
+			final NamingEnumeration<SearchResult> ne = ctx.search("", filter, sc);
+
+			while (ne.hasMore()) {
+				final SearchResult result = ne.next();
+				final Attributes attributes = result.getAttributes();
+				List<ModificationItem> mods = null;
+
+				// Determine applicable TypeMapper for the referencing object
+				final TypeMapping m = getMapping(directory.makeAbsoluteName(result
+						.getName()), attributes.get("objectClass"), mappers);
+
+				if (null == m) {
+					logger
+							.warn("Could not determine TypeMapping for referencing object at "
+									+ result.getName());
+					continue;
+				}
+
+				for (final ReferenceAttributeMapping ra : refererAttributes) {
+					// check whether the reference matches the type of object we found
+					if (ra.getTypeMapping() != m)
+						continue;
+
+					final Attribute attr = attributes.get(ra.getFieldName());
+					if (attr != null) {
+						// for rename: re-add new name
+						if (null != newDN && null == mods) {
+							mods = new LinkedList<ModificationItem>();
+
+							attr.remove(oldDN);
+							attr.add(newDN);
+
+							mods
+									.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr));
+						}
+
+						if (null == mods) {
+							mods = new LinkedList<ModificationItem>();
+							attr.remove(oldDN);
+
+							// check whether we need to re-add the dummy member
+							if (attr.size() == 0
+									&& (ra.getCardinality() == Cardinality.ONE || ra
+											.getCardinality() == Cardinality.ONE_OR_MANY))
+								attr.add(directory.getDummyMember());
+
+							mods
+									.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr));
+						}
+					}
+				}
+
+				if (null != mods) {
+					final ModificationItem[] modsArray = mods
+							.toArray(new ModificationItem[mods.size()]);
+
+					DiropLogger.LOG.logModify(result.getName(), modsArray,
+							"cascading update due to DN change of referenced object");
+
+					ctx.modifyAttributes(result.getName(), modsArray);
+				}
+			}
+		}
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	SecondLevelCache getSecondLevelCache() {
+		return secondLevelCache;
+	}
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/OneToManyMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/OneToManyMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/OneToManyMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/OneToManyMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,345 @@
+/*******************************************************************************
+ * 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.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.Name;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class maps the outgoing side of one-to-many (which are actually always
+ * many-to-many) style mappings. It usually corresponds to the attribute holding
+ * the member reference of a group type mapped by {@link GroupMapping}.
+ * 
+ * @author levigo
+ */
+public class OneToManyMapping extends ReferenceAttributeMapping {
+
+	private static final Logger logger = LoggerFactory.getLogger(OneToManyMapping.class);
+
+	private final Class memberType;
+	private TypeMapping memberMapping;
+
+	private static final Set EMPTY = Collections.unmodifiableSet(new HashSet());
+
+	public OneToManyMapping(String fieldName, String memberType)
+			throws ClassNotFoundException {
+		super(fieldName, Set.class.getName());
+		if (memberType.equals("*"))
+			this.memberType = Object.class;
+		else
+			this.memberType = Class.forName(memberType);
+
+		this.cardinality = Cardinality.ONE_OR_MANY;
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#valueFromAttributes(javax.naming.directory.Attribute)
+	 */
+	@Override
+	protected Object valueFromAttributes(final Attributes attributes,
+			final Object o, final Transaction tx) throws NamingException,
+			DirectoryException {
+		// make proxy for lazy loading
+		return Proxy.newProxyInstance(o.getClass().getClassLoader(),
+				new Class[]{getFieldType()}, new InvocationHandler() {
+					private Set realMemberSet;
+
+					public Object invoke(Object proxy, Method method, Object[] args)
+							throws Throwable {
+						if (null == realMemberSet) {
+							if (DiropLogger.LOG.isReadEnabled())
+								DiropLogger.LOG.logReadComment(
+										"LAZY LOAD: collection for {0}: {1}", fieldName, type
+												.getDN(o));
+
+							realMemberSet = loadMemberSet(attributes);
+							setValue(o, realMemberSet);
+						}
+						return method.invoke(realMemberSet, args);
+					};
+				});
+	}
+
+	/**
+	 * @param attributes
+	 * @param tx TODO
+	 * @return
+	 * @throws DirectoryException
+	 */
+	private Set loadMemberSet(Attributes attributes) throws DirectoryException {
+
+		final Attribute membersAttribute = attributes.get(fieldName);
+
+		final Transaction tx = new Transaction(type.getMapping());
+		try {
+			final Set results = new HashSet();
+			if (null != membersAttribute) {
+				final NamingEnumeration<?> e = membersAttribute.getAll();
+				try {
+
+					while (e.hasMore()) {
+						final String memberDN = e.next().toString();
+
+						// ignore dummy
+						if (type.getDirectoryFacade().isDummyMember(memberDN))
+							continue;
+
+						TypeMapping mm = this.memberMapping;
+						if (null == mm)
+							try {
+								mm = type.getMapping().getMapping(memberDN, tx);
+							} catch (final NameNotFoundException f) {
+								logger.warn("Ignoring nonexistant referenced object: "
+										+ memberDN);
+								continue;
+							}
+
+						if (null == mm) {
+							logger.warn(this + ": can't determine mapping type for dn="
+									+ memberDN);
+							continue;
+						}
+
+						try {
+							results.add(mm.load(memberDN, tx));
+						} catch (final DirectoryException f) {
+							if (f.getCause() != null
+									&& f.getCause() instanceof NameNotFoundException)
+								logger.warn("Ignoring nonexistant referenced object: "
+										+ memberDN);
+							else
+								throw f;
+						}
+					}
+				} finally {
+					e.close();
+				}
+			}
+			return results;
+		} catch (final NamingException e) {
+			throw new DirectoryException(
+					"Exception during lazy loading of group members", e);
+		} finally {
+			tx.commit();
+		}
+	}
+
+	@Override
+	protected void cascadePreSave(Object o, Transaction tx)
+			throws DirectoryException {
+		super.cascadePreSave(o, tx);
+
+		Set memberSet = (Set) getValue(o);
+		if (null == memberSet)
+			memberSet = EMPTY; // empty set
+
+		// if we still see the unchanged proxy, we're done!
+		if (!Proxy.isProxyClass(memberSet.getClass()))
+			for (final Object member : memberSet) {
+				// make sure that the member has already been saved
+				final TypeMapping mappingForMember = getMappingForMember(member);
+				final String dn = mappingForMember.getDN(member);
+				if (null == dn)
+					mappingForMember.save(member, null, tx);
+			}
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#dehydrate(org.openthinclient.common.directory.Object,
+	 *      javax.naming.directory.BasicAttributes)
+	 */
+	@Override
+	public Object dehydrate(Object o, BasicAttributes a)
+			throws DirectoryException, NamingException {
+		Set memberSet = (Set) getValue(o);
+
+		if (null == memberSet)
+			memberSet = EMPTY; // empty set
+
+		// if we still see the unchanged proxy, we're done!
+		if (!Proxy.isProxyClass(memberSet.getClass())) {
+			// compile list of memberDNs
+			// Attribute memberDNs = null;
+			final Attribute memberDNs = new BasicAttribute(fieldName);
+
+			if (memberSet.isEmpty()) {
+				// do we need a dummy entry?
+				if (cardinality == Cardinality.ONE_OR_MANY)
+					memberDNs.add(type.getDirectoryFacade().getDummyMember());
+			} else
+				for (final Object member : memberSet)
+					try {
+						final TypeMapping mappingForMember = getMappingForMember(member);
+
+						final String memberDN = type.getDirectoryFacade().fixNameCase(
+								mappingForMember.getDN(member));
+
+						memberDNs.add(memberDN);
+					} catch (final NamingException e) {
+						throw new DirectoryException("Can't dehydrate", e);
+					}
+
+			// we only add the attribute if it has members
+			if (memberDNs.size() > 0)
+				a.put(memberDNs);
+
+		} else
+			a.put(new BasicAttribute(fieldName,
+					TypeMapping.ATTRIBUTE_UNCHANGED_MARKER));
+
+		return memberSet;
+	}
+
+	private TypeMapping getMappingForMember(Object member)
+			throws DirectoryException {
+		TypeMapping mappingForMember = memberMapping;
+
+		// for a generic mapping we have no way of accessing
+		// the DN of the member object without fetching at least the default
+		// mapping for it.
+		if (null == mappingForMember)
+			mappingForMember = type.getMapping().getMapping(member.getClass());
+
+		if (null == mappingForMember)
+			throw new DirectoryException(
+					"One-to-many associaction contains a member of type "
+							+ member.getClass() + " for which I don't have a mapping.");
+
+		final String dn = mappingForMember.getDN(member);
+
+		// if the member doesn't have a dn, we must resort to the default mapping
+		if (null == dn)
+			return mappingForMember;
+
+		// if the mapping we found doesn't match the dn, we need
+		// to refine it: the member may point to a non-default directory
+		// for the mapped type.
+		Name parsedDN;
+		try {
+			parsedDN = mappingForMember.getDirectoryFacade().getNameParser()
+					.parse(dn);
+			if (!mappingForMember.getDirectoryFacade().contains(parsedDN)) {
+				mappingForMember = type.getMapping().getMapping(member.getClass(), dn);
+
+				// re-parse, because the provider might be different.
+				// We may want to get rid of other provider types (besides SUN),
+				// because of this unnecessary complexity.
+				parsedDN = mappingForMember.getDirectoryFacade().getNameParser().parse(
+						dn);
+			}
+
+			return mappingForMember;
+		} catch (final NamingException e) {
+			throw new DirectoryException("Unable to determine mapping for member", e);
+		}
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#cascadePostSave(org.openthinclient.common.directory.Object)
+	 */
+	@Override
+	protected void cascadePostSave(Object o, Transaction tx, DirContext ctx)
+			throws DirectoryException {
+		Set memberSet = (Set) getValue(o);
+		if (null == memberSet)
+			memberSet = EMPTY; // empty set
+
+		// if we still see the unchanged proxy, we're done!
+		if (!Proxy.isProxyClass(memberSet.getClass()))
+			for (final Object member : memberSet) {
+				final TypeMapping mm = getMappingForMember(member);
+
+				if (null == mm)
+					throw new DirectoryException(this
+							+ ": set contains member of unmapped type: " + member.getClass());
+
+				mm.save(member, null, tx);
+			}
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#checkNull(javax.naming.directory.Attributes)
+	 */
+	@Override
+	protected boolean checkNull(Attributes a) {
+		return false;
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#initNewInstance(org.openthinclient.common.directory.Object)
+	 */
+	@Override
+	protected void initNewInstance(Object instance) throws DirectoryException {
+		// set new empty collection
+		setValue(instance, EMPTY);
+	}
+
+	/*
+	 * @see org.openthinclient.common.directory.ldap.AttributeMapping#initPostLoad()
+	 */
+	@Override
+	protected void initPostLoad() {
+		super.initPostLoad();
+
+		// we don't set up the member mapping, if this group accepts any kind of
+		// member
+		if (!memberType.equals(Object.class)) {
+			final TypeMapping member = type.getMapping().getMapping(memberType);
+			if (null == member)
+				throw new IllegalStateException(this + ": no mapping for member type "
+						+ memberType);
+
+			this.memberMapping = member;
+
+			member.addReferrer(this);
+		}
+	}
+
+	@Override
+	public void setCardinality(String c) {
+		super.setCardinality(c);
+
+		if (cardinality != Cardinality.MANY
+				&& cardinality != Cardinality.ONE_OR_MANY)
+			throw new IllegalArgumentException("Illegal cardinality " + cardinality
+					+ " for " + type.getMappedType() + "." + fieldName);
+	}
+
+	@Override
+	Cardinality getCardinality() {
+		return cardinality;
+	}
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RDNAttributeMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RDNAttributeMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RDNAttributeMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RDNAttributeMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * 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.regex.Pattern;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+
+/**
+ * A special mapping for the RDN attribute which quotes/unquotes the name in
+ * order to satisfy LDAP name escaping rules.
+ * 
+ * @author levigo
+ */
+public class RDNAttributeMapping extends AttributeMapping {
+
+  /**
+   * @param fieldName
+   * @param fieldType
+   * @throws ClassNotFoundException
+   */
+  public RDNAttributeMapping(String fieldName)
+      throws ClassNotFoundException {
+    super(fieldName, "java.lang.String");
+  }
+
+  private static final Pattern QUOTE_TO_LDAP = Pattern.compile("[\\\\,=]");
+  private static final String QUOTE_REPLACEMENT = "\\\\$0";
+
+  /*
+   * @see org.apache.directory.odm.AttributeMapping#valueToAttributes(javax.naming.directory.BasicAttributes,
+   *      java.lang.Object)
+   */
+  @Override
+  protected Object valueToAttributes(BasicAttributes a, Object v) {
+    assert v instanceof String;
+    return super.valueToAttributes(a, QUOTE_TO_LDAP.matcher((String) v)
+        .replaceAll(QUOTE_REPLACEMENT));
+  }
+
+  /** The Pattern used to un-quote a value from ldap escaping */
+  private static final Pattern UNQUOTE_FROM_LDAP = Pattern.compile("\\",
+      Pattern.LITERAL);
+
+  /*
+   * @see org.apache.directory.odm.AttributeMapping#valueFromAttributes(javax.naming.directory.Attributes,
+   *      java.lang.Object)
+   */
+  @Override
+  protected Object valueFromAttributes(Attributes a, Object o, Transaction tx)
+      throws NamingException, DirectoryException {
+    Object value = super.valueFromAttributes(a, o, tx);
+    if (value instanceof String)
+      return UNQUOTE_FROM_LDAP.matcher((String) value).replaceAll("");
+    else
+      return value;
+  }
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ReferenceAttributeMapping.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ReferenceAttributeMapping.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ReferenceAttributeMapping.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/ReferenceAttributeMapping.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * Abstract superclass for {@link AttributeMapping}s dealing with attributes
+ * which reference objects.
+ */
+abstract class ReferenceAttributeMapping extends AttributeMapping {
+	public ReferenceAttributeMapping(String fieldName, String fieldType)
+			throws ClassNotFoundException {
+		super(fieldName, fieldType);
+	}
+
+	abstract Cardinality getCardinality();
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackAction.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackAction.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackAction.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackAction.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * @author levigo
+ */
+public interface RollbackAction {
+  public void performRollback() throws DirectoryException;
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackException.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackException.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackException.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/RollbackException.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * @author levigo
+ */
+public class RollbackException extends DirectoryException {
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * @param message
+   * @param cause
+   */
+  public RollbackException(Throwable cause) {
+    super(
+        "Can't roll back transaction. The directory may be in an inconsistent state now.",
+        cause);
+  }
+}

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/SecondLevelCache.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/SecondLevelCache.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/SecondLevelCache.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/SecondLevelCache.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * 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.io.IOException;
+
+import javax.naming.Name;
+import javax.naming.directory.Attributes;
+
+public interface SecondLevelCache {
+
+	/**
+	 * Get the cache entry associated with the given Name.
+	 * 
+	 * @param name
+	 * @return
+	 */
+	public abstract Attributes getEntry(Name name);
+
+	/**
+	 * Purge the cache entry associated with the given name.
+	 * 
+	 * @param name
+	 * @return
+	 * @throws IllegalStateException
+	 */
+	public abstract boolean purgeEntry(Name name) throws IllegalStateException;
+
+	/**
+	 * Store a cache entry for the given name.
+	 * 
+	 * @param name
+	 * @param object
+	 */
+	public abstract void putEntry(Name name, Attributes a);
+
+	public abstract void clear() throws IllegalStateException, IOException;
+
+}
\ No newline at end of file

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

Added: directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Transaction.java
URL: http://svn.apache.org/viewvc/directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Transaction.java?rev=606228&view=auto
==============================================================================
--- directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Transaction.java (added)
+++ directory/sandbox/hennejg/odm/trunk/src/main/java/org/apache/directory/odm/Transaction.java Fri Dec 21 08:03:46 2007
@@ -0,0 +1,367 @@
+/*******************************************************************************
+ * 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.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.Name;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class models an LDAP transaction. Right now it serves the following
+ * purposes:
+ * <ul>
+ * <li>To detect cycles during cascading operations and
+ * <li>To handle the rollback of failed transactions.
+ * <li>To act as a transaction-scoped cache
+ * </ul>
+ * The latter is necessary since LDAP doesn't support atomic transactions
+ * spanning several entities. In order to perform a rollback, the system has to
+ * issue compensating actions in reverse order.
+ * 
+ * @author levigo
+ */
+public class Transaction {
+	/**
+	 * Special attribute ID used to store the a {@link TypeMapping}'s hash code
+	 * in a cache element's attributes for later retrieval.
+	 */
+	private static final String TYPE_MAPPING_KEY = "####TypeMappingKey####";
+
+	private static final Logger logger = LoggerFactory.getLogger(Transaction.class);
+
+	/**
+	 * Set of processed entities during a cascading operation. Used to detect
+	 * cycles.
+	 */
+	private final Set processedEntities = new HashSet();
+
+	/**
+	 * A list of actions to perform in order to roll back a transaction.
+	 */
+	private final List<RollbackAction> rollbackActions = new LinkedList<RollbackAction>();
+
+	private final Map<Name, Object> cache = new HashMap<Name, Object>();
+
+	/**
+	 * The Mapping that initiated this transaction.
+	 */
+	private final Mapping mapping;
+
+	/**
+	 * The Contexts opened by this transaction.
+	 */
+	private final Map<DirectoryFacade, DirContext> contextCache = new HashMap<DirectoryFacade, DirContext>();
+
+	private final boolean disableGlobalCache;
+
+	boolean isClosed = false;
+
+	public Transaction(Mapping mapping) {
+		this(mapping, false);
+	}
+
+	public Transaction(Mapping mapping, boolean disableGlobalCache) {
+		this.mapping = mapping;
+		this.disableGlobalCache = disableGlobalCache;
+	}
+
+	/**
+	 * Copy constructor. Copies just the ContextFactory from the other
+	 * transaction.
+	 * 
+	 * @param tx
+	 */
+	public Transaction(Transaction tx) {
+		this(tx.mapping, tx.disableGlobalCache);
+	}
+
+	/**
+	 * Add an entity which has been processed (saved/updated) during the
+	 * transaction.
+	 * 
+	 * @param entity
+	 */
+	public void addEntity(Object entity) {
+		assertNotClosed();
+
+		processedEntities.add(entity);
+	}
+
+	/**
+	 * Returns whether the given entity has already been processed during the
+	 * transaction.
+	 * 
+	 * @param entity
+	 * @return
+	 */
+	public boolean didAlreadyProcessEntity(Object entity) {
+		assertNotClosed();
+
+		return processedEntities.contains(entity);
+	}
+
+	/**
+	 * Add an action to perform during rollback.
+	 * 
+	 * @param action
+	 */
+	public void addRollbackAction(RollbackAction action) {
+		rollbackActions.add(action);
+	}
+
+	/**
+	 * Roll back the transaction by applying all RollbackActions in reverse order.
+	 * If one of the actions fail, the rollback continues to undo as much work as
+	 * possible.
+	 */
+	public void rollback() throws RollbackException {
+		assertNotClosed();
+
+		try {
+			if (logger.isDebugEnabled())
+				logger.debug("ROLLBACK: Need to apply " + rollbackActions.size()
+						+ " RollbackActions.");
+
+			final ListIterator<RollbackAction> i = rollbackActions
+					.listIterator(rollbackActions.size());
+			Throwable firstCause = null;
+			while (i.hasPrevious()) {
+				try {
+					i.previous().performRollback();
+				} catch (final Throwable e) {
+					if (null != firstCause)
+						firstCause = e;
+					logger
+							.error(
+									"Exception during Rollback. Trying to continue with rollback anyway.",
+									e);
+				}
+
+				if (null != firstCause)
+					throw new RollbackException(firstCause);
+			}
+		} finally {
+			try {
+				closeContexts();
+			} catch (final NamingException e) {
+				logger.error("Exception during commit - rolling back", e);
+			}
+		}
+	}
+
+	/**
+	 * Get an entry from the cache.
+	 * 
+	 * @param name
+	 * @return
+	 * @throws Exception
+	 */
+	public Object getCacheEntry(Name name) throws Exception {
+		assertNotClosed();
+
+		final Object cached = cache.get(name);
+
+		if (null != cached) {
+			if (logger.isDebugEnabled())
+				logger.debug("TX cache hit for " + name);
+			return cached;
+		}
+
+		if (!disableGlobalCache) {
+			// got it in the second level cache?
+			final SecondLevelCache slc = mapping.getSecondLevelCache();
+			if (null != slc) {
+				final Attributes cachedAttributes = slc.getEntry(name);
+				if (null != cachedAttributes) {
+					if (logger.isDebugEnabled())
+						logger.debug("Global cache hit for " + name);
+
+					// re-create a new object instance from the cached attributes.
+					final Attribute a = cachedAttributes.get(TYPE_MAPPING_KEY);
+					if (null == a)
+						// should not happen
+						logger.error("No type mapping key in cached attributes");
+					else {
+						final int hashCode = ((Integer) a.get()).intValue();
+						cachedAttributes.remove(TYPE_MAPPING_KEY);
+
+						// find type mapping. FIXME: we may want to get rid of the linear
+						// search
+						for (final TypeMapping m : mapping.getMappers())
+							if (hashCode == m.hashCode()) {
+								// resurrect instance from attributes
+								final Object instance = m.createInstanceFromAttributes(name
+										.toString(), cachedAttributes, this);
+
+								// tx didn't have it yet!
+								cache.put(name, instance);
+
+								return instance;
+							}
+					}
+				}
+			}
+		}
+
+		return null;
+	}
+
+	private void assertNotClosed() {
+		if (isClosed)
+			throw new IllegalStateException("Transaction already closed");
+	}
+
+	/**
+	 * Put an entry into the cache. This method updates the first-level
+	 * (transaction-scoped) cache as well as the second-level (mapping-scoped)
+	 * cache.
+	 * 
+	 * @param m TODO
+	 * @param name
+	 * @param value
+	 */
+	public void putCacheEntry(TypeMapping m, Name name, Object value, Attributes a) {
+		assertNotClosed();
+
+		cache.put(name, value);
+
+		final SecondLevelCache slc = mapping.getSecondLevelCache();
+		if (null != slc) {
+			a.put(TYPE_MAPPING_KEY, m.hashCode());
+			slc.putEntry(name, a);
+		}
+	}
+
+	/**
+	 * @throws RollbackException
+	 * 
+	 */
+	public void commit() throws RollbackException {
+		assertNotClosed();
+
+		try {
+			closeContexts();
+		} catch (final NamingException e) {
+			logger.error("Exception during commit - rolling back", e);
+			rollback();
+		}
+	}
+
+	/**
+	 * @throws NamingException
+	 * 
+	 */
+	private void closeContexts() throws NamingException {
+		if (contextCache.size() == 0)
+			logger.debug("Closed without having opened a Context");
+
+		for (final DirContext ctx : contextCache.values())
+			ctx.close();
+		contextCache.clear();
+
+		isClosed = true;
+	}
+
+	@Override
+	protected void finalize() throws Throwable {
+		if (contextCache.size() > 0) {
+			logger.error("Internal error: disposed incompletely closed Transaction");
+
+			// clean up.
+			closeContexts();
+		}
+
+		super.finalize();
+	}
+
+	/**
+	 * @param name
+	 */
+	public void purgeCacheEntry(Name name) {
+		assertNotClosed();
+
+		cache.remove(name);
+
+		final SecondLevelCache slc = mapping.getSecondLevelCache();
+		if (null != slc)
+			slc.purgeEntry(name);
+	}
+
+	public DirContext getContext(DirectoryFacade connectionDescriptor)
+			throws DirectoryException {
+		assertNotClosed();
+
+		DirContext ctx = contextCache.get(connectionDescriptor);
+		if (null == ctx)
+			try {
+				ctx = openContext(connectionDescriptor);
+				contextCache.put(connectionDescriptor, ctx);
+				logger.debug("Created a Context for " + connectionDescriptor);
+			} catch (final NamingException e) {
+				throw new DirectoryException("Can't open connection", e);
+			}
+		return ctx;
+	}
+
+	private DirContext openContext(DirectoryFacade connectionDescriptor)
+			throws NamingException {
+		final DirContext ctx = connectionDescriptor.createDirContext();
+
+		if (connectionDescriptor.getLDAPEnv().get(
+				Mapping.PROPERTY_FORCE_SINGLE_THREADED) != null)
+			// Construct a dynamic proxy which forces all calls to the
+			// context
+			// to happen in a globally synchronized fashion.
+			return (DirContext) Proxy.newProxyInstance(getClass().getClassLoader(),
+					new Class[]{DirContext.class}, new InvocationHandler() {
+						public Object invoke(Object proxy, Method method, Object[] args)
+								throws Throwable {
+							synchronized (Mapping.class) { // sync globally
+								try {
+									return method.invoke(ctx, args);
+								} catch (final Exception e) {
+									throw e.getCause();
+								}
+							}
+						};
+					});
+
+		return ctx;
+	}
+
+	public boolean isClosed() {
+		return isClosed;
+	}
+}

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