Return-Path: Delivered-To: apmail-directory-commits-archive@www.apache.org Received: (qmail 90305 invoked from network); 21 Dec 2007 16:55:23 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.2) by minotaur.apache.org with SMTP; 21 Dec 2007 16:55:23 -0000 Received: (qmail 43425 invoked by uid 500); 21 Dec 2007 16:55:12 -0000 Delivered-To: apmail-directory-commits-archive@directory.apache.org Received: (qmail 43388 invoked by uid 500); 21 Dec 2007 16:55:12 -0000 Mailing-List: contact commits-help@directory.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@directory.apache.org Delivered-To: mailing list commits@directory.apache.org Delivered-To: moderator for commits@directory.apache.org Received: (qmail 62711 invoked by uid 99); 21 Dec 2007 16:04:20 -0000 X-ASF-Spam-Status: No, hits=-100.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: 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... Date: Fri, 21 Dec 2007 16:03:49 -0000 To: commits@directory.apache.org From: hennejg@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20071221160354.8C8CF1A983A@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org 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-null + * 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 defaultMappers = new HashMap(); + + private boolean initialized; + + /** + * All mappers mamaged by this mapping + */ + private final Set mappers = new HashSet(); + + /** + * The mappers indexed by connection descriptor (i.e. target Directory Server) + */ + private final Map> mappersByDirectory = new HashMap>(); + + /** + * The mappers indexed by mapped type + */ + private final Map> mappersByType = new HashMap>(); + + /** + * 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 mappersForClass = mappersByType.get(typeMapping + .getMappedType()); + if (null == mappersForClass) { + mappersForClass = new HashSet(); + mappersByType.put(typeMapping.getMappedType(), mappersForClass); + } + + mappersForClass.add(typeMapping); + + // maintain index by directory + final DirectoryFacade lcd = typeMapping.getDirectoryFacade(); + if (null != lcd) { + Set mappersForConnection = mappersByDirectory.get(lcd); + if (null == mappersForConnection) { + mappersForConnection = new HashSet(); + 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 create(Class 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 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 null + * if it is not (yet?) known. + * + * @return + * @throws NamingException + */ + TypeMapping getMapping(Class type, DirectoryFacade connectionDescriptor) { + final Set 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 null + * 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 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> 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 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 mappings) + throws NamingException, InvalidNameException { + // build list of mapping candidates. There may be more than one! + final List candidates = new ArrayList(); + 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 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 Set list(Class 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 + * @param type + * @param filter + * @param baseDN + * @param scope + * @return + * @throws DirectoryException + */ + @SuppressWarnings("cast") + public Set list(Class 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) 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 load(Class 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 true the cache will not be consulted for + * this object. + * @return + * @throws DirectoryException + */ + public T load(Class 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 forType = mappersByType.get(tm.getMappedType()); + if (null != forType) { + forType.remove(tm); + if (forType.isEmpty()) + mappersByType.remove(tm.getMappedType()); + } + + // maintain index by connection + final Set 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(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 null 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> e : mappersByDirectory + .entrySet()) { + final Set mappers = e.getValue(); + final DirectoryFacade directory = e.getKey(); + + // Build list of referrer attributes. + final Set refererAttributes = new HashSet(); + 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 attributeNames = new HashSet(); + final Set effectiveMappers = new HashSet(); + 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 ne = ctx.search("", filter, sc); + + while (ne.hasMore()) { + final SearchResult result = ne.next(); + final Attributes attributes = result.getAttributes(); + List 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(); + + attr.remove(oldDN); + attr.add(newDN); + + mods + .add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr)); + } + + if (null == mods) { + mods = new LinkedList(); + 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: + *
    + *
  • To detect cycles during cascading operations and + *
  • To handle the rollback of failed transactions. + *
  • To act as a transaction-scoped cache + *
+ * 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 rollbackActions = new LinkedList(); + + private final Map cache = new HashMap(); + + /** + * The Mapping that initiated this transaction. + */ + private final Mapping mapping; + + /** + * The Contexts opened by this transaction. + */ + private final Map contextCache = new HashMap(); + + 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 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