I'd really like to thank everyone for the feedback, it's been really helpful. Here's 
the first version of the code. It works, but has a few issues. See the class comments 
for details. I'm going to be gone for the next week but if anyone has time to test 
this and suggest/make improvements I'd really appreciate it. I've tested this with Sun 
JDK 1.4.1_01 on Win2k with the Slide 2.0/Tomcat 5 bundle and Novell's eDirectory 8.6.2.

Below is the java code and a sample Domain.xml.


== Java ===============================

package org.apache.slide.store.txjndi;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.apache.slide.common.AbstractXAService;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.ServiceConnectionFailedException;
import org.apache.slide.common.ServiceDisconnectionFailedException;
import org.apache.slide.common.ServiceInitializationFailedException;
import org.apache.slide.common.ServiceParameterErrorException;
import org.apache.slide.common.ServiceParameterMissingException;
import org.apache.slide.common.ServiceResetFailedException;
import org.apache.slide.common.Uri;
import org.apache.slide.content.NodeProperty;
import org.apache.slide.content.NodeRevisionContent;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.NodeRevisionNumber;
import org.apache.slide.content.RevisionAlreadyExistException;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.content.RevisionNotFoundException;
import org.apache.slide.store.ContentStore;
import org.apache.slide.store.NodeStore;
import org.apache.slide.store.RevisionDescriptorStore;
import org.apache.slide.store.RevisionDescriptorsStore;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.structure.SubjectNode;
import org.apache.slide.util.logger.Logger;

/**
 * <p>
 * This is a read-only Store implementation for retrieving Slide users
 * and roles from an LDAP server. It has been tested with Slide 2.0 (the
 * Tomcat 5 binary bundle) and Novell's eDirectory 8.6.2. It is very slow
 * (caching helps but renders the Store almost useless for production) and
 * still needs a lot of work. It currently implements ContentStore,
 * NodeStore, RevisionDescriptorStore and RevisionDescriptorsStore.
 * Another Store implementation must be used for SecurityStore and
 * LockStore since there is no way to get reasonable values for this data
 * from LDAP.
 * </p>
 * <h3>Prerequisites</h3>
 * <p>
 * To use this Store your app server must be setup to authenticate
 * users using the LDAP server. For Tomcat 5 @see
 * http://jakarta.apache.org/tomcat/tomcat-5.0-doc/realm-howto.html#JNDIRealm.
 * You cannot use the Slide Realm to authenticate users because this Store
 * does not expose a <span style="font-style: italic;">password</span>
 * property.
 * </p>
 * <h3>Store Parameters</h3>
 * <p>
 * Parameters used in Domain.xml when setting up the Store.
 * </p>
 * <dl>
 * <dt>jndi.container</dt>
 * <dd>The base LDAP context you wish to search. Example: 
<i>ou=Users,o=AdventistHealth</i></dd>
 * </dl>
 * <dl>
 * <dt>jndi.attributes.rdn</dt>
 * <dd>The attribute used to uniquely identify the objects you're fetching. Usually 
uid or cn.</dd>
 * </dl>
 * <dl>
 * <dt>jndi.search.filter</dt>
 * <dd>The filter string to use for the search. Example: 
<i>(objectClass=inetOrgPerson)</i>. @see 
http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/directory/DirContext.html#search(javax.naming.Name,%20java.lang.String,%20javax.naming.directory.SearchControls)</dd>
 * </dl>
 * <dl>
 * <dt>jndi.search.scope</dt>
 * <dd>The Scope of the search. Can be one of <i>OBJECT_SCOPE</i>, 
<i>ONELEVEL_SCOPE</i>, <i>SUBTREE_SCOPE</i>. @see
 * 
http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/directory/SearchControls.html#OBJECT_SCOPE</dd>
 * </dl>
 * <dl>
 * <dt>jndi.search.attributes</dt>
 * <dd>A comma delimited list of the attributes you want returned with your search 
results. Example: <i>givenName, uid, mail</i></dd>
 * </dl>
 * <dl>
 * <dt>java.naming.*</dt>
 * <dd>Parameters for connecting to the LDAP server. @see 
http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/InitialContext.html</dd>
 * </dl>
 * <h3>TODO:</h3>
 * <ol>
 * <li>
 * I'd like to see this implemented as a ResourceManager rather than
 * a stand-alone Store. I think it would fit into Slide's framework better
 * that way and mean less duplicated code.
 * </li>
 * <li>
 * Performance needs help. It takes about 15 seconds to retrieve
 * 20,000 objects from an LDAP server, but it take far longer than that to
 * get an uncached listing from the /users directory. If performance can't
 * be improved then a caching solution needs to be found that will allow
 * the cache to be refreshed (either periodically or event based).
 * </li>
 * <li>
 * I think there's still room for a full-fledged LDAP store. The way
 * LDAP exposes a directory as a graph-of-objects-with-properties and
 * Slide exposes a repository as a graph-of-objects-with-properties seems
 * very similar to me ;). However, adapting the structure of most LDAP
 * servers to the user/role structure that Slide uses would be a bit of a
 * pain, so I don't think this kind of Store would be useful for
 * users/roles in Slide. I have heard of people using LDAP to keep track
 * of server inventories and things like that, though, and I think it
 * would work well there.
 * </li>
 * </ol>
 *
 * @author <a href="mailto:[EMAIL PROTECTED]">James Mason</a>
 */
public class JNDIPrincipalStore
                extends AbstractXAService
                implements ContentStore, NodeStore, RevisionDescriptorStore,
                        RevisionDescriptorsStore {

        public static final String JNDI_PROPERTY_PREFIX = "java.naming";

        public static final String JNDI_CONTAINER_PARAMETER = "jndi.container";

        public static final String JNDI_FILTER_PARAMETER = "jndi.search.filter";

        public static final String JNDI_RDN_ATTRIBUTE_PARAMETER = 
"jndi.attributes.rdn";
        
        public static final String JNDI_GROUPMEMBERSET_PARAMETER = 
"jndi.attributes.groupmemberset";
        
        public static final String JNDI_SEARCH_ATTRIBUTES_PARAMETER = 
"jndi.search.attributes";

        public static final String JNDI_SEARCH_SCOPE_PARAMETER = "jndi.search.scope";
        
        public static final String LDAP_NAMESPACE = "LDAP:";
        
        public static final String LOG_CHANNEL = JNDIPrincipalStore.class.getName();
        
        // TODO - figure out how to get this from the Domain.
        public static final String USERS_SCOPE = "/users";

        protected Hashtable ctxParameters;

        protected DirContext ctx;

        protected boolean isConnected = false;

        protected String container;

        protected String filter;

        protected String rdnAttribute;

        protected int searchScope;
        
        protected String[] descriptorAttributes;
        
        protected String groupMemberSet;

        
        public JNDIPrincipalStore() {
                ctxParameters = new Hashtable();
        }
        
        // ----------------------------------------------------------- Service Methods 
--------

        public void initialize( NamespaceAccessToken token )
                        throws ServiceInitializationFailedException {
                super.initialize( token );
        }

        public void setParameters( Hashtable parameters )
                        throws ServiceParameterErrorException,
                        ServiceParameterMissingException {

                Iterator keys = parameters.keySet().iterator();
                while ( keys.hasNext() ) {
                        String key = (String)keys.next();
                        if ( key.startsWith( JNDI_PROPERTY_PREFIX ) ) {
                                ctxParameters.put( key, parameters.get( key ) );
                        }
                }
                
                container = (String)parameters.get( JNDI_CONTAINER_PARAMETER );
                filter = (String)parameters.get( JNDI_FILTER_PARAMETER );
                rdnAttribute = (String)parameters.get( JNDI_RDN_ATTRIBUTE_PARAMETER );
                
                String ss = (String)parameters.get( JNDI_SEARCH_SCOPE_PARAMETER );
                if ( ss.equals( "OBJECT_SCOPE" ) ) {
                        searchScope = SearchControls.OBJECT_SCOPE;
                } else if ( ss.equals( "ONELEVEL_SCOPE" ) ) {
                        searchScope = SearchControls.ONELEVEL_SCOPE;
                } else if ( ss.equals( "SUBTREE_SCOPE" ) ) {
                        searchScope = SearchControls.SUBTREE_SCOPE;
                }
                
                String searchAttributesString = (String)parameters.get(
                        JNDI_SEARCH_ATTRIBUTES_PARAMETER );
                
                ArrayList searchAttributesList = new ArrayList();
                StringTokenizer tok = new StringTokenizer( searchAttributesString, "," 
);
                while ( tok.hasMoreTokens() ) {
                        searchAttributesList.add( tok.nextToken().trim() );
                }
                
                String gms = (String)parameters.get( JNDI_GROUPMEMBERSET_PARAMETER );
                if ( gms != null ) {
                        searchAttributesList.add( gms );
                        groupMemberSet = gms;
                } else {
                        groupMemberSet = "";
                }
                
                descriptorAttributes = (String[])searchAttributesList.toArray( new 
String[0] );
                
        }
        
        public boolean cacheResults() {
                // TODO - Either make jndi lookups faster or fix caching
                // There is currently no way (that I know of) to clear the cache. 
Since all
                // of the information this store displays is managed externally to 
Slide
                // there needs to be a way to tell Slide to update the cached objects.
                // This means either extending Slide's caching or writing a new 
implementation
                // just for this store.
                return true;
        }

        
        //------------------------------------------------------ NodeStore Methods 
----------
        
        public void storeObject( Uri uri, ObjectNode object ) throws 
ServiceAccessException, ObjectNotFoundException {}

        public void createObject( Uri uri, ObjectNode object ) throws 
ServiceAccessException, ObjectAlreadyExistsException {}

        public void removeObject( Uri uri, ObjectNode object ) throws 
ServiceAccessException, ObjectNotFoundException {}

        public ObjectNode retrieveObject( Uri uri ) throws ServiceAccessException,
                        ObjectNotFoundException {
                
                getLogger().log( "Calling retrieveObject(" + uri.toString() + ").",
                        LOG_CHANNEL, Logger.DEBUG );
                
                Uri parentUri = uri.getParentUri();
                String objectName = getObjectNameFromUri( uri );

                SubjectNode node = new SubjectNode( uri.toString() );

                // As long as this node isn't the root node create a parent binding.
                // This doesn't appear to make any difference, but just in case.
                if ( !uri.toString().equals( "/" ) ) {
                        SubjectNode parentNode = new SubjectNode( parentUri.toString() 
);
                        node.addParentBinding( objectName, parentNode );
                }

                // If the uri matches the scope create a SubjectNode with bindings for 
all
                // of the results from a jndi search
                if ( uri.isStoreRoot() ) {
                                        
                        SearchControls controls = new SearchControls();
                        controls.setSearchScope( searchScope );
                        
                        try {
                                NamingEnumeration results = ctx.search(
                                        container,
                                        filter,
                                        controls );
        
                                if ( !results.hasMore() ) {
                                        getLogger().log(
                                                "No objects found in container " + 
container +
                                                " that match filter " + filter + ".",
                                                LOG_CHANNEL,
                                                Logger.WARNING );
                                }
                                while ( results.hasMore() ) {
                                        SearchResult result = null;
                                        try {
                                                result = (SearchResult)results.next();
                                        } catch ( NamingException e ) {
                                                getLogger().log(
                                                        "Error getting next search 
result.",
                                                        e, LOG_CHANNEL, Logger.ERROR );
                                        }
                                        String name = result.getName();
                                        
                                        String value = parseLdapName( name );
                                        
                                        SubjectNode childNode = new SubjectNode(
                                                uri.toString() + "/" + value );
                                        node.addBinding( value, childNode );

                                }
                        } catch ( NamingException e ) {
                                getLogger().log(
                                        "Error during search.",
                                        e, LOG_CHANNEL, Logger.ERROR );
                        }
                } else {
                        // If the uri matches the scope + something else try to do a 
lookup
                        // of the "+ something" in LDAP.
                        
                        try {
                                if ( ctx.lookup(
                                                rdnAttribute + "=" + objectName + "," +
                                                        container ) == null ) {
                                        throw new ObjectNotFoundException( uri );
                                }
                        } catch ( NamingException e ) {
                                getLogger().log(
                                        "Error retrieving " + uri.toString(),
                                        e, LOG_CHANNEL, Logger.ERROR );
                                throw new ServiceAccessException( this, e );
                        }
                }
                                
                return node;
                
        }

        
        //-------------------------------------------- RevisionDescriptorStore Methods 
--------
        
        public void createRevisionDescriptor( Uri uri, NodeRevisionDescriptor 
revisionDescriptor ) throws ServiceAccessException {}

        public void storeRevisionDescriptor( Uri uri, NodeRevisionDescriptor 
revisionDescriptor ) throws ServiceAccessException, 
RevisionDescriptorNotFoundException {}

        public void removeRevisionDescriptor( Uri uri, NodeRevisionNumber 
revisionNumber ) throws ServiceAccessException {}

        public NodeRevisionDescriptor retrieveRevisionDescriptor( Uri uri,
                        NodeRevisionNumber revisionNumber ) throws 
ServiceAccessException,
                        RevisionDescriptorNotFoundException {

                getLogger().log( "Calling retrieveRevisionDescriptor(" + 
uri.toString() + ").",
                        LOG_CHANNEL, Logger.DEBUG );
                
                String objectName = getObjectNameFromUri( uri );
                
                Hashtable props = new Hashtable();
                
                String resourceType = "<collection/>";
                if ( !uri.isStoreRoot() ) {
                        resourceType += "<principal/>";
                }
                props.put(
                        "DAV:resourcetype",
                        new NodeProperty( "resourcetype", resourceType, "DAV:", "", 
false ) );
                props.put(
                        "DAV:displayname",
                        new NodeProperty( "displayname", objectName, "DAV:", "", false 
) );
                
                // The storeRoot isn't a real object so it doesn't have any parameters 
to look up
                if ( !uri.isStoreRoot() ) {
                                                
                        String localFilter = rdnAttribute + "=" + objectName;
                
                        SearchControls controls = new SearchControls();
                        controls.setSearchScope( searchScope );
                        controls.setReturningAttributes( descriptorAttributes );
                        
                        try {
                                NamingEnumeration results = ctx.search(
                                        container,
                                        localFilter,
                                        controls );
        
                                if ( !results.hasMore() ) {
                                        throw new RevisionDescriptorNotFoundException( 
uri.toString() );
                                }
                                while ( results.hasMore() ) {
                                        SearchResult result = null;
                                        try {
                                                result = (SearchResult)results.next();
                                        } catch ( NamingException e ) {
                                                getLogger().log(
                                                        "Error getting search result 
with filter: " + localFilter +
                                                        " from container: " + 
container + ".",
                                                        LOG_CHANNEL, Logger.ERROR );
                                                throw new ServiceAccessException( 
this, e );
                                        }
                                        
                                        NamingEnumeration attributes = 
result.getAttributes().getAll();
                                        while ( attributes.hasMore() ) {
                                                Attribute attribute = 
(Attribute)attributes.next();
                                                StringBuffer valueString = new 
StringBuffer();
                                                boolean isGms = 
attribute.getID().equals( groupMemberSet );
                                                boolean isMva = attribute.size() > 1;
                                                for ( int i = 0; i < attribute.size(); 
i++ ) {
                                                        try {
                                                                Object value = 
attribute.get( i );
                                                                if ( !( value 
instanceof String ) ) {
                                                                        
getLogger().log(
                                                                                
"Non-string value found for " + attribute.getID() +
                                                                                       
 ".",
                                                                                
LOG_CHANNEL,
                                                                                
Logger.DEBUG );
                                                                        continue;
                                                                }
                                                                if ( isGms ) {
                                                                        
valueString.append( "<D:href xmlns:D='DAV:'>" );
                                                                        
valueString.append( USERS_SCOPE ).append( "/" );
                                                                        
valueString.append( parseLdapName( value.toString() ) );
                                                                        
valueString.append( "</D:href>" );
                                                                } else {
                                                                        if ( isMva ) {
                                                                                
valueString.append( "<mva xmlns=\"" )
                                                                                       
 .append( LDAP_NAMESPACE ).append( "\">" );
                                                                                
valueString.append( value.toString().toLowerCase() );
                                                                                
valueString.append( "</mva>" );
                                                                        } else {
                                                                                
valueString.append( value.toString().toLowerCase() );
                                                                        }
                                                                }
                                                        } catch ( NamingException e ) {
                                                                getLogger().log(
                                                                        "Error 
fetching next attribute value for attribute " +
                                                                                
attribute.getID() + ".",
                                                                        e, 
LOG_CHANNEL, Logger.DEBUG );
                                                        }
                                                }
                                                
                                                if ( isGms ) {
                                                        props.put(
                                                                "DAV:group-member-set",
                                                                new NodeProperty(
                                                                        
"group-member-set",
                                                                        
valueString.toString(),
                                                                        "DAV:" ) );
                                                        
                                                } else {
                                                        props.put(
                                                                LDAP_NAMESPACE + 
attribute.getID(),
                                                                new NodeProperty(
                                                                        
attribute.getID(),
                                                                        
valueString.toString(),
                                                                        LDAP_NAMESPACE 
) );
                                                }
                                        }
                                }
                        } catch ( NamingException e ) {
                                getLogger().log(
                                        "Error during search.",
                                        e, LOG_CHANNEL, Logger.ERROR );
                        }
                }
                
                return new NodeRevisionDescriptor(
                        new NodeRevisionNumber( 1, 0 ),
                        "main",
                        new Vector(),
                        props );
                
        }
        
        // --------------------------------------------- RevisionDescriptorsStore 
Methods -----

        public NodeRevisionDescriptors retrieveRevisionDescriptors( Uri uri ) throws 
ServiceAccessException, RevisionDescriptorNotFoundException {
                getLogger().log( "Calling retrieveRevisionDescriptors(" + 
uri.toString() + ").",
                        LOG_CHANNEL, Logger.INFO );
                
                        NodeRevisionNumber rev = new NodeRevisionNumber( 1, 0 );
                
                        Hashtable workingRevisions = new Hashtable();
                        workingRevisions.put( "1.0", rev );
                        
                        Hashtable latestRevisionNumbers = new Hashtable();
                        latestRevisionNumbers.put( "1.0", rev );
                
                        // From looking at NodeRevisionDescriptors.cloneObject I see 
branchNames is
                        // supposed to store Vector. I'm guessing the Vector is of 
revision numbers
                        Vector branches = new Vector();
                        branches.add( rev );
                        Hashtable branchNames = new Hashtable();
                        branchNames.put( "main", branches );
                        
                return new NodeRevisionDescriptors(
                        uri.toString(),
                        rev,
                        workingRevisions,
                        latestRevisionNumbers,
                        branchNames,
                        false );
        }

        public void createRevisionDescriptors( Uri uri, NodeRevisionDescriptors 
revisionDescriptors ) throws ServiceAccessException {}

        public void storeRevisionDescriptors( Uri uri, NodeRevisionDescriptors 
revisionDescriptors ) throws ServiceAccessException, 
RevisionDescriptorNotFoundException {}

        public void removeRevisionDescriptors( Uri uri ) throws ServiceAccessException 
{}
        
        
        // --------------------------------------------------------- XA Methods 
--------------
        
        public void connect() throws ServiceConnectionFailedException {
                try {
                        ctx = new InitialDirContext( ctxParameters );
                        isConnected = true;
                } catch ( NamingException e ) {
                        getLogger().log("Error Connecting to LDAP Server",
                                e,
                                LOG_CHANNEL,
                                Logger.EMERGENCY );
                        throw new ServiceConnectionFailedException( this, e );
                }
        }

        public void disconnect() throws ServiceDisconnectionFailedException {
                try {
                        ctx.close();
                } catch ( NamingException e ) {
                        getLogger().log( "Error disconnecting from LDAP",
                                e, LOG_CHANNEL, Logger.WARNING );
                        ctx = null;
                } finally {
                        isConnected = false;
                }
        }

        public void reset() throws ServiceResetFailedException {}

        public boolean isConnected() throws ServiceAccessException {
                return isConnected;
        }

        public int getTransactionTimeout() throws XAException {
                return 0;
        }

        public boolean setTransactionTimeout( int seconds ) throws XAException {
                return false;
        }

        public boolean isSameRM( XAResource rm ) throws XAException {
                return false;
        }

        public Xid[] recover( int flag ) throws XAException {
                return new Xid[0];
        }

        public int prepare( Xid txId ) throws XAException {
                return XA_RDONLY;
        }

        public void forget( Xid txId ) throws XAException {}

        public void rollback( Xid txId ) throws XAException {}

        public void end( Xid txId, int flags ) throws XAException {}

        public void start( Xid txId, int flags ) throws XAException {}

        public void commit( Xid txId, boolean onePhase ) throws XAException {}
        
        // -------------------------------------------------- ContentStore Methods 
----------

        public NodeRevisionContent retrieveRevisionContent( Uri uri, 
NodeRevisionDescriptor revisionDescriptor ) throws ServiceAccessException, 
RevisionNotFoundException {
                return new NodeRevisionContent();
        }

        public void createRevisionContent( Uri uri, NodeRevisionDescriptor 
revisionDescriptor, NodeRevisionContent revisionContent ) throws 
ServiceAccessException, RevisionAlreadyExistException {}

        public void storeRevisionContent( Uri uri, NodeRevisionDescriptor 
revisionDescriptor, NodeRevisionContent revisionContent ) throws 
ServiceAccessException, RevisionNotFoundException {}

        public void removeRevisionContent( Uri uri, NodeRevisionDescriptor 
revisionDescriptor ) throws ServiceAccessException {}
        
        // --------------------------------------------------- Helper Methods 
---------------
        
        protected String parseLdapName( String name ) {

                // Since attribute values can contain pretty much anything, parsing
                // name to get the attribute value isn't terribly accurate.
                // The slow way is to find a value for the attribute that matches the
                // results from getName(), but that is horribly horribly slow.
                // On the assumption that "," is more likely to be in the value than
                // "=", this should work most of the time and be faster... I hope.
                
                int firstEqual = name.indexOf( "=" );
                if ( firstEqual < 0 ) {
                        firstEqual = 0;
                }
                
                int secondEqual = name.substring( firstEqual + 1 ).indexOf( "=" );
                if ( secondEqual < 0 ) {
                        secondEqual = name.length() - 1;
                } else {
                        secondEqual = secondEqual + firstEqual + 1;
                }
                
                int end = name.substring( 0, secondEqual ).lastIndexOf( "," );
                if ( end < 0 ) {
                        end = name.length();
                }
                
                String value = name.substring( firstEqual + 1, end ).toLowerCase();
                return value;
                
        }

        protected String getObjectNameFromUri( Uri uri ) {
                String objectName = uri.toString().substring(
                        uri.toString().lastIndexOf( "/" ) + 1 );
                return objectName.toLowerCase();
        }

}



== Domain.xml ==================================

 � my email client is being picky. I'll post this in a follow up email �



James Mason
Adventist Health
Programmer/Analyst
916.783.2576
[EMAIL PROTECTED]

---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to