/*
 * $Header: /home/cvs/jakarta-slide/src/stores/org/apache/slide/store/txjndi/JNDIPrincipalStore.java,v 1.5.2.3 2004/10/27 05:23:36 masonjm Exp $
 * $Revision: 1.5.2.3 $
 * $Date: 2004/10/27 05:23:36 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     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.slide.store.txjndi;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.TreeSet;
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 net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.ObjectExistsException;

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.lock.LockTokenNotFoundException;
import org.apache.slide.lock.NodeLock;
import org.apache.slide.security.NodePermission;
import org.apache.slide.store.ContentStore;
import org.apache.slide.store.LockStore;
import org.apache.slide.store.NodeStore;
import org.apache.slide.store.RevisionDescriptorStore;
import org.apache.slide.store.RevisionDescriptorsStore;
import org.apache.slide.store.SecurityStore;
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.CacheManagerFactory;
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 Novell's eDirectory
 * version 8.6.2. Other LDAP servers should work.
 * </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
 * <a href="http://jakarta.apache.org/tomcat/tomcat-5.0-doc/realm-howto.html#JNDIRealm">this</a>.
 * You cannot use the SlideRealm 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>cache.refresh.checkrate</dt>
 * <dd>
 * How often, in <em>seconds</em>, the cache refresh thread should check for Uris in the cache
 * that need to be refreshed. The default value is "15".
 * See <a href="#cacherefreshing">Cache Refreshing</a> for more information.
 * </dd>
 * 
 * <dt>cache.refresh.rate</dt>
 * <dd>
 * How frequently, in <em>seconds</em>, Uris that are marked for refreshing should be refreshed.
 * This value must be less than the TimeToLive and TimeToIdle (whichever is least) parameters
 * specified for the EHCache in order for the items to never expire. The default value is "800".
 * See <a href="#caching">Caching</a> and <a href="#cacherefreshing">Cache Refreshing</a> for
 * more information.
 * </dd>
 * 
 * <dt>cache.refresh.threshold</dt>
 * <dd>
 * The maximum amount of time, in <em>milliseconds</em>, that retrieve* methods can take before
 * the Uri they are retrieving is scheduled for refreshing. By tuning this parameter you can keep
 * smaller, infrequently accessed Uris from perpetually remaining in the cache. This may improve
 * cache performance.
 * See <a href="#cacherefreshing">Cache Refreshing</a> for more information.
 * </dd>
 * 
 * <dt>jndi.container</dt>
 * <dd>
 * The base LDAP context you wish to search. Example: <em>ou=Users,o=Company</em>
 * </dd>
 * 
 * <dt>jndi.attributes.rdn</dt>
 * <dd>
 * The attribute used to uniquely identify the objects you're fetching. Usually uid or cn.
 * </dd>
 * 
 * <dt>jndi.attributes.userprincipalname</dt>
 * <dd>
 * The attribute used to provide a user/role name which is mapped into Slide instead of the
 * path name. This attribute is optional.
 * </dd>
 * 
 * <dt>jndi.search.filter</dt>
 * <dd>
 * The filter string to use for the search. Example: <em>(objectClass=inetOrgPerson)</em>.
 * The default value is <em>(objectClass=*)</em>.
 * See the <a href="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)">DirContext.search()</a> javadoc.
 * </dd>
 * 
 * <dt>jndi.search.scope</dt>
 * <dd>
 * The Scope of the search. Can be one of <em>OBJECT_SCOPE</em>, <em>ONELEVEL_SCOPE</em>,
 * <em>SUBTREE_SCOPE</em>. The default value is <em>ONELEVEL_SCOPE</em>.
 * See the <a href="http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/directory/SearchControls.html#OBJECT_SCOPE">SearchControls</a> javadoc.
 * </dd>
 * 
 * <dt>jndi.search.attributes</dt>
 * <dd>
 * A comma delimited list of the attributes you want returned with your search results.
 * Example: <em>givenName, uid, mail</em>. The default value is <em>cn</em>.
 * </dd>
 * 
 * <dt>java.naming.*</dt>
 * <dd>
 * Parameters for connecting to the LDAP server.
 * See the <a href="http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/InitialContext.html">InitialContext</a> javadoc.
 * </dd>
 * </dl>
 * 
 * <h3><a name="caching">Caching</a></h3>
 * <p>
 * This Store makes use of <a href="http://ehcache.sourceforge.net/">EHCache</a>. You will
 * need ehcache.jar in order to use this Store. When initialized the default CacheManager is
 * used to find a Cache named "org.apache.slide.store.txjndi.JNDIPrincipalStore". If there is
 * no Cache found with this name then a Cache is created with these default values:
 * </p>
 * <ul>
 * 	<li>name = org.apache.slide.store.txjndi.JNDIPrincipalStore</li>
 * 	<li>maxElementsInMemory = 200</li>
 * 	<li>eternal = false</li>
 * 	<li>timeToIdleSeconds = 900</li>
 * 	<li>timeToLiveSeconds = 900</li>
 * 	<li>overflowToDisk = true</li>
 * </ul>
 * <p>
 * To override these values you will need to create a configuration file for EHCache with a
 * cache named "org.apache.slide.store.txjndi.JNDIPrincipalStore" that has the settings you
 * wish. See the documentation at the <a href="http://ehcache.sourceforge.net/">EHCache website</a>
 * for instructions.
 * </p>
 * 
 * <h3><a name="cacherefreshing">Cache Refreshing</a></h3>
 * <p>
 * Because the data delivered by this Store is managed externally to Slide the data cache must be
 * periodically expired to pick up any changes. Because creating object Nodes can take a long time
 * for LDAP queries that return a lot of objects, larger queries need to be preemptively refreshed
 * before a user makes a request to an expired object.
 * </p>
 * <p>
 * The retrieveObject() and retrieveNodeDescriptor() methods monitor the amount of time it takes
 * them to return for each Uri. If the method takes longer than a specified amount of time
 * (configured with the <em>cache.refresh.threshold</em> parameter) the Uri is marked as needing
 * to be refreshed. Upon initialization the Store spawns a child thread that periodically checks
 * for Uris that need to be refreshed.
 * </p>
 * 
 * <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>
 * 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>
 *
 */
public class JNDIPrincipalStore
		extends AbstractXAService
		implements ContentStore, LockStore, NodeStore, RevisionDescriptorStore,
			RevisionDescriptorsStore, SecurityStore {

	public static final String CACHE_NAME              = JNDIPrincipalStore.class.getName();
	public static final String CACHE_OBJECT_PREFIX     = "object: ";
	public static final String CACHE_DESCRIPTOR_PREFIX = "descriptor: ";
	
	public static final String JNDI_PROPERTY_PREFIX = "java.naming";

	// Parameter keys
	public static final String PARAM_CACHE_REFRESH_CHECK_RATE = "cache.refresh.checkrate";
	public static final String PARAM_CACHE_REFRESH_RATE       = "cache.refresh.rate";
	public static final String PARAM_CACHE_REFRESH_THRESHOLD  = "cache.refresh.threshold";
	
	public static final String PARAM_JNDI_CONTAINER         = "jndi.container";
	public static final String PARAM_JNDI_FILTER            = "jndi.search.filter";
	public static final String PARAM_JNDI_GROUPMEMBERSET    = "jndi.attributes.groupmemberset";
	public static final String PARAM_JNDI_RDN_ATTRIBUTE     = "jndi.attributes.rdn";
	public static final String PARAM_JNDI_SEARCH_ATTRIBUTES = "jndi.search.attributes";
	public static final String PARAM_JNDI_SEARCH_SCOPE      = "jndi.search.scope";
	public static final String PARAM_JNDI_USERPRINCIPALNAME = "jndi.attributes.userprincipalname";
	
	public static final String PARAM_LOG_VALIDATION_ERRORS = "log.validationerrors";
    
	// Default values
	public static final int     DEFAULT_CACHE_SIZE               = 200;
	public static final boolean DEFAULT_CACHE_OVERFLOW_TO_DISK   = true;
	public static final boolean DEFAULT_CACHE_ETERNAL            = false;
	public static final long    DEFAULT_CACHE_TTL                = 900L; // seconds
	public static final long    DEFAULT_CACHE_TTI                = 900L; // seconds
	public static final long    DEFAULT_CACHE_REFRESH_CHECK_RATE = 15L;  // seconds
	public static final long    DEFAULT_CACHE_REFRESH_RATE       = 800L; // seconds
	public static final long    DEFAULT_CACHE_REFRESH_THRESHOLD  = 200L; // milliseconds!!
	
	public static final String DEFAULT_JNDI_SEARCH_ATTRIBUTES = "cn";
	public static final String DEFAULT_JNDI_FILTER            = "(objectClass=*)";
	
	public static final String LDAP_NAMESPACE = "LDAP:";
	
	public static final String LOG_CHANNEL = JNDIPrincipalStore.class.getName();
	
	protected Hashtable ctxParameters;

	protected boolean isConnected = false;
	protected boolean logValidationErrors = false;

	protected Cache cache;
	protected TreeSet refreshList;
	protected long refreshRate            = DEFAULT_CACHE_REFRESH_RATE;
	protected long refreshThreadSleepTime = DEFAULT_CACHE_REFRESH_CHECK_RATE;
	protected long refreshThreshold       = DEFAULT_CACHE_REFRESH_THRESHOLD;
	protected RefreshThread refresher;
	
	protected String container;
	protected String[] descriptorAttributes;
	protected String filter;
	protected String groupMemberSet;
	protected String rdnAttribute;
	protected int searchScope;
	protected String principalNameAttribute;

	private String name;
	private String usersPath;
	private Map objectNameMap; // Uri-String -> LDAP lookup name
    
	
	public JNDIPrincipalStore() {
		ctxParameters = new Hashtable();
		cache = getCache();
		name = "";
		refreshList = new TreeSet();
		refresher = new RefreshThread();
		objectNameMap = new HashMap();
	}
	
	// ----------------------------------------------------------- Service Methods --------

	public void initialize( NamespaceAccessToken token )
			throws ServiceInitializationFailedException {
		name = "JNDIPrincipalStore[" + scope.toString() + "]";
		getLogger().log( "Initializing " + name, LOG_CHANNEL, Logger.DEBUG );
		super.initialize( token );
		
		usersPath = token.getNamespaceConfig().getUsersPath();
	}

	public void setParameters( Hashtable parameters )
			throws ServiceParameterErrorException,
			ServiceParameterMissingException {
		
		getLogger().log( "Setting parameters for " + name, LOG_CHANNEL, Logger.DEBUG );
		
		String temp = null;

		// Set ctxParameters
		Iterator keys = parameters.keySet().iterator();
		while ( keys.hasNext() ) {
			temp = (String)keys.next();
			if ( temp.startsWith( JNDI_PROPERTY_PREFIX ) ) {
				ctxParameters.put( temp, parameters.get( temp ) );
			}
		}
		
		// Set container
		container = (String)parameters.get( PARAM_JNDI_CONTAINER );
		if ( container == null || container.length() == 0 ) {
			getLogger().log(
				"Error during " + name + " setup: No value set for " +
				PARAM_JNDI_CONTAINER,
				LOG_CHANNEL,
				Logger.CRITICAL );
		}
		
		// Set filter
		filter = (String)parameters.get( PARAM_JNDI_FILTER );
		if ( filter == null || filter.length() == 0 ) {
			filter = DEFAULT_JNDI_FILTER;
		}
		
		// Set rdnAttribute
		rdnAttribute = (String)parameters.get( PARAM_JNDI_RDN_ATTRIBUTE );
		if ( rdnAttribute == null || rdnAttribute.length() == 0 ) {
			getLogger().log(
				"Error during " + name + " setup: No value set for " +
				PARAM_JNDI_RDN_ATTRIBUTE,
				LOG_CHANNEL,
				Logger.CRITICAL );
		}
		
		// Set searchScope
		temp = (String)parameters.get( PARAM_JNDI_SEARCH_SCOPE );
		if ( "OBJECT_SCOPE".equalsIgnoreCase( temp ) ) {
			searchScope = SearchControls.OBJECT_SCOPE;
		} else if ( "ONELEVEL_SCOPE".equalsIgnoreCase( temp ) ) {
			searchScope = SearchControls.ONELEVEL_SCOPE;
		} else if ( "SUBTREE_SCOPE".equalsIgnoreCase( temp ) ) {
			searchScope = SearchControls.SUBTREE_SCOPE;
		} else {
			searchScope = SearchControls.ONELEVEL_SCOPE;
		}
		
		// Set descriptorAttributes and groupMemberSet
		temp = (String)parameters.get( PARAM_JNDI_SEARCH_ATTRIBUTES );
		if ( temp == null || temp.length() == 0 ) {
			temp = DEFAULT_JNDI_SEARCH_ATTRIBUTES;
		}
		ArrayList searchAttributesList = new ArrayList();
		StringTokenizer tok = new StringTokenizer( temp, "," );
		while ( tok.hasMoreTokens() ) {
			searchAttributesList.add( tok.nextToken().trim() );
		}
		String gms = (String)parameters.get( PARAM_JNDI_GROUPMEMBERSET );
		if ( gms != null ) {
			searchAttributesList.add( gms );
			groupMemberSet = gms;
		} else {
			groupMemberSet = "";
		}
		descriptorAttributes = (String[])searchAttributesList.toArray( new String[0] );
		
		// Set refreshRate
		temp = (String)parameters.get( PARAM_CACHE_REFRESH_RATE );
		if ( temp != null ) {
			refreshRate = Long.parseLong( temp ) * 1000L;
		} else {
			refreshRate = DEFAULT_CACHE_REFRESH_RATE * 1000L;
		}
		
		// Set refreshThreadSleepTime
		temp = (String)parameters.get( PARAM_CACHE_REFRESH_CHECK_RATE );
		if ( temp != null ) {
			refreshThreadSleepTime = Long.parseLong( temp ) * 1000L;
		} else {
			refreshThreadSleepTime = DEFAULT_CACHE_REFRESH_CHECK_RATE * 1000L;
		}
		
		// Set refreshThreshold
		temp = (String)parameters.get( PARAM_CACHE_REFRESH_THRESHOLD );
		if ( temp != null ) {
			refreshThreshold = Long.parseLong( temp );
		} else {
			refreshThreshold = DEFAULT_CACHE_REFRESH_THRESHOLD;
		}
		
		// Set logValidationErrors
		temp = (String)parameters.get( PARAM_LOG_VALIDATION_ERRORS );
		if ( "true".equalsIgnoreCase( temp ) ) {
			logValidationErrors = true;
		}

		//Set attribute which contains the user principal name for authentication
		principalNameAttribute = (String)parameters.get(PARAM_JNDI_USERPRINCIPALNAME);
	}
	
	public boolean cacheResults() {
		return false;
	}

	
	//------------------------------------------------------ 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( name + ": Retrieving Object " + uri.toString() + ".",
			LOG_CHANNEL, Logger.DEBUG );
		
		String cacheKey = CACHE_OBJECT_PREFIX + uri.toString();
		
		Element cachedNode = null;
		try {
			if ( cache != null ) {
				cachedNode = cache.get( cacheKey );
			}
		} catch ( CacheException e ) {
			getLogger().log(
				name + ": Error while getting \"" + cacheKey + "\" from cache.",
				e,
				LOG_CHANNEL,
				Logger.ERROR);
		}
		if ( cachedNode != null ) {
			getLogger().log(
				name + ": ObjectNode for \"" + uri.toString() + "\" found in cache.",
				LOG_CHANNEL,
				Logger.DEBUG );
			return (ObjectNode)cachedNode.getValue();
		} else {
			return getObject( uri );
		}
		
	}

	//-------------------------------------------- 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( name + ": Retrieving Revision Descriptor for " + uri.toString() + ".",
			LOG_CHANNEL, Logger.DEBUG );
		
		String cacheKey = CACHE_DESCRIPTOR_PREFIX + uri.toString();
		
		Element cachedDescriptor = null;
		try {
			if ( cache != null ) {
				cachedDescriptor = cache.get( cacheKey );
			}
		} catch ( CacheException e ) {
			getLogger().log(
				name + ": Error while getting \"" + cacheKey + "\" from cache.",
				e,
				LOG_CHANNEL,
				Logger.ERROR);
		}
		if ( cachedDescriptor != null ) {
			getLogger().log(
				name + ": NodeRevisionDescriptor for \"" + uri.toString() + "\" found in cache.",
				LOG_CHANNEL,
				Logger.DEBUG );
			return (NodeRevisionDescriptor)cachedDescriptor.getValue();
		} else {
			return getRevisionDescriptor( uri );
		}
		
	}
	
	// --------------------------------------------- RevisionDescriptorsStore Methods -----

	public NodeRevisionDescriptors retrieveRevisionDescriptors( Uri uri )
			throws ServiceAccessException, RevisionDescriptorNotFoundException {
		getLogger().log( name + ": Retrieving Revision Descriptors for " + uri.toString() + ".",
			LOG_CHANNEL, Logger.DEBUG );
		
		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 a 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 {
		if ( !refresher.isAlive() ) {
			refresher.start();
		}
	}

    public void disconnect() throws ServiceDisconnectionFailedException {
		if ( refresher.isAlive() ) {
			refresher.halt();
		}
	}

    public void reset() throws ServiceResetFailedException {}

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

	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 {
		NodeRevisionContent nrc = new NodeRevisionContent();
		nrc.setContent( new char[0] );
		return nrc;
	}

	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 {}
	
	//	 --------------------------------------------------- Security Store Methods ---------------

	public void grantPermission(Uri uri, NodePermission permission) throws ServiceAccessException {
		// TODO Auto-generated method stub
		
	}

	public void revokePermission(Uri uri, NodePermission permission) throws ServiceAccessException {
		// TODO Auto-generated method stub
		
	}

	public void revokePermissions(Uri uri) throws ServiceAccessException {
		// TODO Auto-generated method stub
		
	}

	/**
	 * Always returns read access for all users.
	 */
	public Enumeration enumeratePermissions(Uri uri) throws ServiceAccessException {
		Vector permissions = new Vector();
		permissions.add( new NodePermission( uri.toString(), "all", "/actions/read" ) );
		return permissions.elements();
	}
	
	//	 --------------------------------------------------- LockStore Methods ---------------

	public void putLock(Uri uri, NodeLock lock) throws ServiceAccessException {
		// TODO Auto-generated method stub
		
	}

	public void renewLock(Uri uri, NodeLock lock) throws ServiceAccessException, LockTokenNotFoundException {
		// TODO Auto-generated method stub
		
	}

	public void removeLock(Uri uri, NodeLock lock) throws ServiceAccessException, LockTokenNotFoundException {
		// TODO Auto-generated method stub
		
	}

	public void killLock(Uri uri, NodeLock lock) throws ServiceAccessException, LockTokenNotFoundException {
		// TODO Auto-generated method stub
		
	}

	public Enumeration enumerateLocks(Uri uri) throws ServiceAccessException {
		return new Vector().elements();
	}
	
	// --------------------------------------------------- Worker Methods ---------------
	
	protected SubjectNode getObject( Uri uri )
			throws ObjectNotFoundException, ServiceAccessException {
		
		long start = System.currentTimeMillis();
        DirContext ctx = null;
        try {
            ctx = getContext();
        } catch ( ServiceConnectionFailedException e ) {
            throw new ServiceAccessException(this, e);
        }
        Uri parentUri = uri.getParentUri();
		String objectName = getObjectNameFromUri( uri );
		
		Vector parentBindings = new Vector();
		Vector childBindings = new Vector();

		// As long as this node isn't the root node create a parent binding.
		// This doesn't appear to do anything, but just in case.
		if ( !uri.toString().equals( "/" ) ) {
			parentBindings.add( new ObjectNode.Binding( objectName, parentUri.toString() ) );
		}
		
		SearchControls controls = new SearchControls();
		controls.setSearchScope( searchScope );
		controls.setReturningAttributes( descriptorAttributes );
		
		// If the uri matches the scope create a SubjectNode with bindings for all
		// of the results from a jndi search
		if ( uri.isStoreRoot() ) {
			
			try {
				NamingEnumeration results = ctx.search(
					container,
					filter,
					controls );

				if ( !results.hasMore() ) {
					getLogger().log(
						name + ": 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(
							name + ": Error getting next search result.",
							e, LOG_CHANNEL, Logger.ERROR );
					}
					String name = result.getName();
					if ( !validatePathName( name ) ) {
						continue;
					}
					String value = parseLdapName(name);
					if (principalNameAttribute != null) {
						String uriValue = ((String)result.getAttributes().get(principalNameAttribute).get()).toLowerCase();
						objectNameMap.put(uriValue, value);
						value = uriValue;
					}
					
					getLogger().log(
						name + ": Creating child binding \"" + value + "\" for \"" +
						uri.toString() + "\".",
						LOG_CHANNEL, Logger.DEBUG );
					
					childBindings.add(
						new ObjectNode.Binding( value, uri.toString() + "/" + value ) );

				}
			} catch ( NamingException e ) {
				getLogger().log(
					name + ": Error during search.",
					e, LOG_CHANNEL, Logger.ERROR );
			}
		} else {
			// If the uri matches the scope + something try to do a lookup
			// of the "+ something" in LDAP.
			
			try {
				if (principalNameAttribute != null && objectNameMap.get(objectName) == null)
					retrieveObject(parentUri);
				NamingEnumeration results = ctx.search(
					container,
					rdnAttribute + "=" + (principalNameAttribute != null ? (String)objectNameMap.get(objectName) : objectName),
					controls);

				if ( !results.hasMore() ) {
                    if (ctx != null) {
                        closeContext(ctx);
                    }
					throw new ObjectNotFoundException( uri );
				}
			} catch ( NamingException e ) {
				getLogger().log(
					name + ": Error retrieving " + uri.toString(),
					e, LOG_CHANNEL, Logger.ERROR );
                if (ctx != null) {
                    closeContext(ctx);
                }
				throw new ServiceAccessException( this, e );
			}
		}

		getLogger().log( name + ": Creating SubjectNode for \"" + uri.toString() + "\".",
			LOG_CHANNEL, Logger.DEBUG );
		
		SubjectNode node = new SubjectNode(
			uri.toString(), childBindings, parentBindings, new Vector() );
		// Workaround for bug in ObjectNode.validate()
		node.setUri( uri.toString() );

		if ( cache != null ) {
			getLogger().log(
				name + ": Putting ObjectNode for " + uri.toString() + " to cache.",
				LOG_CHANNEL,
				Logger.DEBUG );
			Element cachedNode = new Element( CACHE_OBJECT_PREFIX + uri.toString(), node );
			cache.put(cachedNode);
		}
		
		long elapsed = System.currentTimeMillis() - start;
		if ( elapsed > refreshThreshold ) {
			addRefreshee( uri, Refreshee.REFRESH_OBJECT );
		}
        if (ctx != null) {
            closeContext(ctx);
        }		
		return node;
	}

	protected NodeRevisionDescriptor getRevisionDescriptor( Uri uri )
			throws RevisionDescriptorNotFoundException, ServiceAccessException {
		
		long start = System.currentTimeMillis();
        DirContext ctx = null;
        try {
            ctx = getContext();
        } catch ( ServiceConnectionFailedException e ) {
            throw new ServiceAccessException(this, e);
        }
		
		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", (!uri.isStoreRoot() && principalNameAttribute != null?(String)objectNameMap.get(objectName):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 + "=" + (principalNameAttribute != null?(String)objectNameMap.get(objectName):objectName);
		    
			SearchControls controls = new SearchControls();
			controls.setSearchScope( searchScope );
			controls.setReturningAttributes( descriptorAttributes );
			
			try {
				NamingEnumeration results = ctx.search(
					container,
					localFilter,
					controls );

				if ( !results.hasMore() ) {
                    if (ctx != null) {
                        closeContext(ctx);
                    }
					throw new RevisionDescriptorNotFoundException( uri.toString() );
				}
				while ( results.hasMore() ) {
					SearchResult result = null;
					try {
						result = (SearchResult)results.next();
					} catch ( NamingException e ) {
						getLogger().log(
							name + ": Error getting search result with filter: " + localFilter +
							" from container: " + container + ".",
							LOG_CHANNEL, Logger.ERROR );
                        if (ctx != null) {
                            closeContext(ctx);
                        }
						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(
										name + ": Non-string value found for " +
										attribute.getID() +	".",
										LOG_CHANNEL,
										Logger.DEBUG );
									continue;
								}
								if ( isGms ) {
									valueString.append( "<D:href xmlns:D='DAV:'>" );
									valueString.append( usersPath ).append( "/" );
									String name = parseLdapName(value.toString());
									if (principalNameAttribute != null) {
										// lookup LDAP user entry
										controls.setReturningAttributes(new String[] { principalNameAttribute });
										NamingEnumeration roleResults =
											ctx.search(container, rdnAttribute + "=" + name, controls);
										if (roleResults.hasMore()) {
											SearchResult userObject = (SearchResult)roleResults.next();
											name = ((String)userObject.getAttributes().get(principalNameAttribute).get()).toLowerCase();
										}
									}
									valueString.append(name);
									valueString.append( "</D:href>" );
								} else {
									if ( isMva ) {
										valueString.append( "<mva xmlns=\"" )
											.append( LDAP_NAMESPACE ).append( "\">" );
										valueString.append( value.toString() );
										valueString.append( "</mva>" );
									} else {
										valueString.append( value.toString() );
									}
								}
							} catch ( NamingException e ) {
								getLogger().log(
									name + ": Error fetching next attribute value for attribute " +
										attribute.getID() + ".",
									e, LOG_CHANNEL, Logger.DEBUG );
							}
						}
						
						if ( isGms ) {
							getLogger().log(
								name + ": Adding property \"group-member-set\" in namespace " +
								"\"DAV:\" with value of \"" + valueString.toString() + "\" to " +
								uri.toString() + ".",
								LOG_CHANNEL, Logger.DEBUG );
							
							props.put(
								"DAV:group-member-set",
								new NodeProperty(
									"group-member-set",
									valueString.toString(),
									"DAV:" ) );
							
						} else {
							getLogger().log(
								name + ": Adding property \"" + attribute.getID() +
								"\" in namespace \"" + LDAP_NAMESPACE + "\" " +
								"with value of \"" +
								valueString.toString() + "\" to " + uri.toString() + ".",
								LOG_CHANNEL, Logger.DEBUG );
							
							props.put(
								LDAP_NAMESPACE + attribute.getID(),
								new NodeProperty(
									attribute.getID(),
									valueString.toString(),
									LDAP_NAMESPACE ) );
						}
					}
				}
			} catch ( NamingException e ) {
				getLogger().log(
					name + ": Error during search.",
					e, LOG_CHANNEL, Logger.ERROR );
			}
		}

		NodeRevisionDescriptor descriptor = new NodeRevisionDescriptor(
			new NodeRevisionNumber( 1, 0 ),
			"main",
			new Vector(),
			props );
		
		if ( cache != null ) {
			getLogger().log(
				name + ": Putting NodeRevisionDescriptor for " + uri.toString() + " to cache.",
				LOG_CHANNEL,
				Logger.DEBUG );
			Element cachedDescriptor = new Element(
				CACHE_DESCRIPTOR_PREFIX + uri.toString(), descriptor );
			cache.put(cachedDescriptor);
		}

		long elapsed = System.currentTimeMillis() - start;
		if ( elapsed > refreshThreshold ) {
			addRefreshee( uri, Refreshee.REFRESH_DESCRIPTOR );
		}
        if (ctx != null) {
            closeContext(ctx);
        }
		return descriptor;
	}
	
	// --------------------------------------------------- Helper Methods ---------------

    /**
     * Closes a JNDI connection.
     * @param ctx the Context to close
     */
    private void closeContext(DirContext ctx) {
        getLogger().log( name + ": Disconnecting from LDAP server.", LOG_CHANNEL, Logger.DEBUG );
        try {
            ctx.close();
        } catch ( NamingException e ) {
            getLogger().log( name + ": Error disconnecting from LDAP",
                e, LOG_CHANNEL, Logger.WARNING );
            ctx = null;
        }
    }
    
    /**
     * Gets a JNDI Context using the connection parameters specified for
     * this Store in the Domain.
     * @throws ServiceConnectionFailedException
     */
    private DirContext getContext() throws ServiceConnectionFailedException {
        getLogger().log( name + ": Connecting to LDAP server.", LOG_CHANNEL, Logger.DEBUG );
        try {
            DirContext ctx = new InitialDirContext( ctxParameters );
            if ( ctx != null ) {
                return ctx;
            } else {
                throw new ServiceConnectionFailedException(
                    this, "Invalid JNDI connection parameters." );
            }
        } catch ( NamingException e ) {
            getLogger().log( name + ": Error Connecting to LDAP Server",
                e,
                LOG_CHANNEL,
                Logger.CRITICAL );
            throw new ServiceConnectionFailedException( this, e );
        }
    }
    
	protected String getObjectNameFromUri( Uri uri ) {
		String objectName = uri.toString().substring(
			uri.toString().lastIndexOf( "/" ) + 1 );
		return objectName.toLowerCase();
	}
	
	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.
		
        if (name.equals("")) return name;

		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;
		
	}

	/**
	 * Checks a String to make sure it is a valid path element.
	 * 
	 * @param name the name to validate
	 * @return false if any of the validation checks failed.
	 */
	protected boolean validatePathName( String name ) {
		boolean valid = true;
		if ( name.indexOf("/") > -1 ) {
			// Skip names that contain "/". They're evil!! (and they break things)
			valid = false;
			if ( logValidationErrors ) {
				getLogger().log(
					name + ": Skipping child with name \"" + name + "\" because " +
					"it contains a /.",
					LOG_CHANNEL, Logger.ERROR );
			}
		}
		return valid;
	}
	
	//	 ----------------------------------------------- Cache Methods/Classes ---------------

	protected synchronized void addRefreshee( Uri uri, int refreshType ) {
		getLogger().log(
			name + ": Adding refreshee for \"" + uri.toString() + "\" of type \"" +
			(refreshType == Refreshee.REFRESH_OBJECT ? "object" : "descriptor") + "\".",
			LOG_CHANNEL,
			Logger.DEBUG );
		
		refreshList.add(
			new Refreshee( uri, System.currentTimeMillis() + refreshRate, refreshType ) );
	}
	
	protected Cache getCache() {
		CacheManager cacheManager = null;
		Cache cache = null;
		try {
			cacheManager = CacheManagerFactory.getDefaultCacheManager();
		} catch ( CacheException e ) {
			getLogger().log(
				name + ": Error getting default CacheManager.",
				e,
				LOG_CHANNEL,
				Logger.ERROR );
			return null;
		}
		cache = cacheManager.getCache( CACHE_NAME );
		if ( cache == null ) {
			cache = new Cache(
				CACHE_NAME,
				DEFAULT_CACHE_SIZE,
				DEFAULT_CACHE_OVERFLOW_TO_DISK,
				DEFAULT_CACHE_ETERNAL,
				DEFAULT_CACHE_TTL,
				DEFAULT_CACHE_TTI );
			try {
				cacheManager.addCache(cache);
			} catch ( IllegalStateException e ) {
				getLogger().log(
					name + ": Error adding cache \"" + CACHE_NAME + "\" to CacheManager.",
					e,
					LOG_CHANNEL,
					Logger.ERROR);
			} catch ( ObjectExistsException e ) {
				getLogger().log(
					name + ": Error adding cache \"" + CACHE_NAME + "\" to CacheManager.",
					e,
					LOG_CHANNEL,
					Logger.ERROR);
			} catch ( CacheException e ) {
				getLogger().log(
					name + ": Error adding cache \"" + CACHE_NAME + "\" to CacheManager.",
					e,
					LOG_CHANNEL,
					Logger.ERROR);
			}
		}
		return cache;
	}
	
	protected synchronized Refreshee getNextRefreshee() {
		Refreshee refreshee = null;
		try {
			refreshee = (Refreshee)refreshList.last();
		} catch ( NoSuchElementException e ) {
			// expected when the list is emtpy
		}
		return refreshee;
	}
	
	protected void refreshCache() {
		Refreshee oldest = getNextRefreshee();
		long now = System.currentTimeMillis();
		while ( oldest != null && oldest.getRefreshTime() < now ) {
			getLogger().log(
				name + ": Refreshing cache for \"" + oldest.getUri().toString() + "\" of type \"" +
				(oldest.getRefreshType() == Refreshee.REFRESH_OBJECT ? "object" : "descriptor") +
				"\".",
				LOG_CHANNEL,
				Logger.DEBUG );
			
			removeRefreshee( oldest );
			try {
				Cache cache = getCache();
				if ( cache != null ) {
					switch( oldest.getRefreshType() ) {
						case Refreshee.REFRESH_OBJECT:
							getObject( oldest.getUri() );
							break;
						case Refreshee.REFRESH_DESCRIPTOR:
							getRevisionDescriptor( oldest.getUri() );
							break;
					}
				}
			} catch ( ObjectNotFoundException e ) {
				getLogger().log(
					name + ": Error refreshing cache for \"" + oldest.getUri().toString() + "\".",
					e,
					LOG_CHANNEL,
					Logger.ERROR );
			} catch ( ServiceAccessException e ) {
				getLogger().log(
					name + ": Error refreshing cache for \"" + oldest.getUri().toString() + "\".",
					e,
					LOG_CHANNEL,
					Logger.ERROR );
			} catch ( RevisionDescriptorNotFoundException e ) {
				getLogger().log(
					name + ": Error refreshing cache for \"" + oldest.getUri().toString() + "\".",
					e,
					LOG_CHANNEL,
					Logger.ERROR );
			}
			oldest = getNextRefreshee();
		}
	}
	
	protected synchronized void removeRefreshee( Refreshee refreshee ) {
		refreshList.remove( refreshee );
	}
	
	/**
	 * Implements Comparable so that the older the object, the sooner it needs to be refreshed,
	 * and therefor the "greater" it is. When used with a SortedSet this ensures objects are
	 * refreshed in the proper order.
	 *
	 */
	protected class Refreshee implements Comparable {
		
		public static final int REFRESH_OBJECT = 0;
		public static final int REFRESH_DESCRIPTOR = 1;
		
		private long refreshTime;
		private Uri uri;
		private int refreshType;
		
		// No default constructor
		private Refreshee() {}
		
		public Refreshee( Uri uri, long refreshTime, int refreshType ) {
			this.refreshTime = refreshTime;
			this.uri = uri;
			this.refreshType = refreshType;
		}
		
		public int compareTo( Object object ) {
			if ( object instanceof Refreshee ) {
				Refreshee other = (Refreshee)object;
				int result = compare( this.getRefreshTime(), other.getRefreshTime() );
				if ( result != 0 ) {
					return result;
				}
			}
			return compare( hashCode(), object.hashCode() );
		}
		
		public long getRefreshTime() {
			return refreshTime;
		}
		
		public int getRefreshType() {
			return refreshType;
		}
		
		public Uri getUri() {
			return uri;
		}
		
		private int compare( long first, long second ) {
			if (first < second) {
				return 1;
			}
			if (second < first) {
				return -1;
			}
			return 0;
		}
	}
	
	protected class RefreshThread extends Thread {
		
		private boolean run;
		
		public RefreshThread() {
			super( "RefreshThread-" + name );
			setPriority( Thread.MIN_PRIORITY );
			setDaemon( false );
		}
		
		public synchronized void halt() {
			run = false;
		}
		
		public void run() {
			run = true;
			while ( keepRunning() ) {
				try {
					Thread.sleep( refreshThreadSleepTime );
				} catch ( InterruptedException e ) {}
				refreshCache();
			}
		}
		
		private synchronized boolean keepRunning() {
			return run;
		}
	}

}