/*
 * $Header: /home/cvspublic/jakarta-slide/src/share/org/apache/slide/store/BindingStore.java,v 1.8 2004/12/21 16:12:44 luetzkendorf Exp $
 * $Revision: 1.8 $
 * $Date: 2004/12/21 16:12:44 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 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;


import java.util.Enumeration;
import java.util.Vector;

import org.apache.slide.common.PropertyName;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.Uri;
import org.apache.slide.common.UriPath;
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.ExtendedStore;
import org.apache.slide.store.NodeStore;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.util.Configuration;
import org.apache.slide.util.XMLValue;
import org.jdom.Element;

/**
 * Store implementation supporting binding-resolution.
 * By extending ExtendedStore, this store implementation inherits also
 * caching.
 *
 * @version   $Revision: 1.8 $
 */
public class BindingStore extends ExtendedStore {
    private static PropertyName RESOURCE_ID = 
        PropertyName.getPropertyName("resource-id");
    private static PropertyName PARENT_SET  = 
        PropertyName.getPropertyName("parent-set");
    
    // overwrites inherited
    public ObjectNode retrieveObject(Uri uri)
        throws ServiceAccessException, ObjectNotFoundException {
        
        if (uri instanceof ResourceId) {
            return super.retrieveObject(uri);
        }
        else {
            return doRetrieveObjectNode(uri);
        }
    }
    
    // overwrites inherited
    public void storeObject(Uri uri, ObjectNode object)
        throws ServiceAccessException, ObjectNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.storeObject(uri, object);
        }
        else {
            ResourceId resourceId = obtainResourceId(uri);
            ObjectNode objectClone = object.cloneObject();
            objectClone.setUri(resourceId.getUuri()); // switch to uuri
            resourceId.getStore().storeObject(resourceId, objectClone);
        }
    }
    
    // overwrites inherited
    public void createObject(Uri uri, ObjectNode object)
        throws ServiceAccessException, ObjectAlreadyExistsException {
        
        if (uri instanceof ResourceId) {
            super.createObject(uri, object);
        }
        else {
            ResourceId resourceId = ResourceId.createNew(uri);
            object.setUuri(resourceId.getUuri());
            ObjectNode objectClone = object.cloneObject();
            objectClone.setUri(resourceId.getUuri()); // switch to uuri
            cacheResolve(uri, resourceId);
            resourceId.getStore().createObject(resourceId, objectClone);
        }
    }
    
    // overwrites inherited
    public void removeObject(Uri uri, ObjectNode object)
        throws ServiceAccessException, ObjectNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.removeObject(uri, object);
        }
        else {
            ResourceId resourceId = obtainResourceId(uri);
            ObjectNode objectClone = object.cloneObject();
            objectClone.setUri(resourceId.getUuri()); // switch to uuri
            cacheResolve(uri, null);
            resourceId.getStore().removeObject(resourceId, objectClone);
        }
    }
    
    // overwrites inherited
    public void grantPermission(Uri uri, NodePermission permission)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.grantPermission(uri, permission);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodePermission permissionClone = permission.cloneObject();
                permissionClone.setObject(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().grantPermission(resourceId, permissionClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void revokePermission(Uri uri, NodePermission permission)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.revokePermission(uri, permission);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodePermission permissionClone = permission.cloneObject();
                permissionClone.setObject(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().revokePermission(resourceId, permissionClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void revokePermissions(Uri uri)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.revokePermissions(uri);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().revokePermissions(resourceId);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public Enumeration enumeratePermissions(Uri uri)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            return super.enumeratePermissions(uri);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                Enumeration permissions = resourceId.getStore().enumeratePermissions(resourceId);
                Vector result = new Vector();
                while (permissions.hasMoreElements()) {
                    NodePermission p = ((NodePermission)permissions.nextElement()).cloneObject();
                    p.setObject(uri.toString()); // switch to uri
                    result.add(p);
                }
                return result.elements();
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void putLock(Uri uri, NodeLock lock)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.putLock(uri, lock);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeLock lockClone = lock.cloneObject();
                lockClone.setObjectUri(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().putLock(resourceId, lockClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void renewLock(Uri uri, NodeLock lock)
        throws ServiceAccessException, LockTokenNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.renewLock(uri, lock);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeLock lockClone = lock.cloneObject();
                lockClone.setObjectUri(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().renewLock(resourceId, lockClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void removeLock(Uri uri, NodeLock lock)
        throws ServiceAccessException, LockTokenNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.removeLock(uri, lock);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeLock lockClone = lock.cloneObject();
                lockClone.setObjectUri(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().removeLock(resourceId, lockClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void killLock(Uri uri, NodeLock lock)
        throws ServiceAccessException, LockTokenNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.killLock(uri, lock);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeLock lockClone = lock.cloneObject();
                lockClone.setObjectUri(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().killLock(resourceId, lockClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public Enumeration enumerateLocks(Uri uri)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            return super.enumerateLocks(uri);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                Enumeration locks = resourceId.getStore().enumerateLocks(resourceId);
                Vector result = new Vector();
                while (locks.hasMoreElements()) {
                    NodeLock l = ((NodeLock)locks.nextElement()).cloneObject();
                    l.setObjectUri(uri.toString()); // switch to uri
                    result.add(l);
                }
                return result.elements();
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public NodeRevisionDescriptors retrieveRevisionDescriptors(Uri uri)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        
        if (uri instanceof ResourceId) {
            return super.retrieveRevisionDescriptors(uri);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeRevisionDescriptors nrdsClone =
                    resourceId.getStore().retrieveRevisionDescriptors(resourceId).cloneObject();
                nrdsClone.setUri(uri.toString());
                return nrdsClone;
            }
            catch (ObjectNotFoundException e) {
                // TODO: throw RevisionDescriptorsNotFoundException???
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void createRevisionDescriptors
        (Uri uri, NodeRevisionDescriptors revisionDescriptors)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.createRevisionDescriptors(uri, revisionDescriptors);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeRevisionDescriptors nrdsClone = revisionDescriptors.cloneObject();
                nrdsClone.setUri(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().createRevisionDescriptors(resourceId, nrdsClone);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void storeRevisionDescriptors
        (Uri uri, NodeRevisionDescriptors revisionDescriptors)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.storeRevisionDescriptors(uri, revisionDescriptors);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                NodeRevisionDescriptors nrdsClone = revisionDescriptors.cloneObject();
                nrdsClone.setUri(resourceId.getUuri()); // switch to uuri
                resourceId.getStore().storeRevisionDescriptors(resourceId, nrdsClone);
            }
            catch (ObjectNotFoundException e) {
                // TODO: throw RevisionDescriptorsNotFoundException???
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void removeRevisionDescriptors(Uri uri)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.removeRevisionDescriptors(uri);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().removeRevisionDescriptors(resourceId);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public NodeRevisionDescriptor retrieveRevisionDescriptor
        (Uri uri, NodeRevisionNumber revisionNumber)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        
        if (uri instanceof ResourceId) {
            return super.retrieveRevisionDescriptor(uri, revisionNumber);
        }
        else {
            try {
                ObjectNode objectNode = doRetrieveObjectNode(uri);
                ResourceId resourceId = obtainResourceId(uri);
                NodeRevisionDescriptor nrd = resourceId.getStore().retrieveRevisionDescriptor(resourceId, revisionNumber);
                nrd.setProperty(RESOURCE_ID, resourceId.asXml());
                nrd.setProperty(PARENT_SET, getXmlParentSet(uri, objectNode));
                return nrd;
            }
            catch (ObjectNotFoundException e) {
                // TODO: throw RevisionDescriptorNotFoundException???
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    public String getXmlParentSet(Uri uri, ObjectNode objectNode) throws ServiceAccessException, ObjectNotFoundException {
        ResourceId resourceId = obtainResourceId(uri);
        // if objectNode is the root of a store, parent-uuri.equals(""),
        // thus we cannot call getFirstMapping because
        // we cannot resolve the uuri
        boolean useBinding = Configuration.useBinding(this) && !resourceId.isStoreRoot();
        
        XMLValue result = new XMLValue();
        Enumeration parentBindings = objectNode.enumerateParentBindings();
        while (parentBindings.hasMoreElements()) {
            ObjectNode.Binding parentBinding = (ObjectNode.Binding) parentBindings.nextElement();
            Element parentElm = new Element("parent", NodeProperty.NamespaceCache.DEFAULT_NAMESPACE);
            Element hrefElm = new Element("href", NodeProperty.NamespaceCache.DEFAULT_NAMESPACE);
            String parentUriStr = UriPath.getParentUri(objectNode.getUri());
            Uri parentUri = new Uri(uri.getToken(), uri.getNamespace(), parentUriStr);
            String uriStr;
            if (useBinding) {
                ResourceId parentResourceId = ResourceId.create(parentUri, parentBinding.getUuri());
                uriStr = getFirstMapping(parentResourceId);
            }
            else {
                uriStr = parentUriStr;
            }
            hrefElm.setText(uriStr);
            parentElm.addContent(hrefElm);
            Element segmentElm = new Element("segment", NodeProperty.NamespaceCache.DEFAULT_NAMESPACE);
            segmentElm.setText(parentBinding.getName());
            parentElm.addContent(segmentElm);
            result.add(parentElm);
        }
        
        return result.toString();
    }
    
    private String getFirstMapping(ResourceId resourceId) throws ServiceAccessException, ObjectNotFoundException {
        String result = "";
        while (!resourceId.isStoreRoot()) {
            ObjectNode objectNode = resourceId.getStore().retrieveObject(resourceId);
            Enumeration bindings = objectNode.enumerateParentBindings();
            if (!bindings.hasMoreElements()) {
                throw new IllegalStateException();
            }
            ObjectNode.Binding parentBinding = (ObjectNode.Binding) bindings.nextElement();
            String parentSegment = parentBinding.getName();
            result = "/" + parentSegment + result;
            resourceId = ResourceId.create(resourceId, parentBinding.getUuri());
        }
        return "/".equals(resourceId.getScope().toString())
            ? result
            : resourceId.getScope()+result;
    }
    
    // overwrites inherited
    public void createRevisionDescriptor
        (Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.createRevisionDescriptor(uri, revisionDescriptor);
        }
        else {
            try {
                revisionDescriptor.removeProperty(RESOURCE_ID);
                revisionDescriptor.removeProperty(PARENT_SET);
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().createRevisionDescriptor(resourceId, revisionDescriptor);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void storeRevisionDescriptor
        (Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.storeRevisionDescriptor(uri, revisionDescriptor);
        }
        else {
            try {
                revisionDescriptor.removeProperty(RESOURCE_ID);
                revisionDescriptor.removeProperty(PARENT_SET);
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().storeRevisionDescriptor(resourceId, revisionDescriptor);
            }
            catch (ObjectNotFoundException e) {
                // TODO: throw RevisionDescriptorNotFoundException???
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void removeRevisionDescriptor(Uri uri, NodeRevisionNumber number)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.removeRevisionDescriptor(uri, number);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().removeRevisionDescriptor(resourceId, number);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public NodeRevisionContent retrieveRevisionContent
        (Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException, RevisionNotFoundException {
        
        if (uri instanceof ResourceId) {
            return super.retrieveRevisionContent(uri, revisionDescriptor);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                return resourceId.getStore().retrieveRevisionContent(resourceId, revisionDescriptor);
            }
            catch (ObjectNotFoundException e) {
                // TODO: throw RevisionNotFoundException???
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void createRevisionContent
        (Uri uri, NodeRevisionDescriptor revisionDescriptor,
         NodeRevisionContent revisionContent)
        throws ServiceAccessException, RevisionAlreadyExistException {
        
        if (uri instanceof ResourceId) {
            super.createRevisionContent(uri, revisionDescriptor, revisionContent);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().createRevisionContent(resourceId, revisionDescriptor, revisionContent);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void storeRevisionContent
        (Uri uri, NodeRevisionDescriptor revisionDescriptor,
         NodeRevisionContent revisionContent)
        throws ServiceAccessException, RevisionNotFoundException {
        
        if (uri instanceof ResourceId) {
            super.storeRevisionContent(uri, revisionDescriptor, revisionContent);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().storeRevisionContent(resourceId, revisionDescriptor, revisionContent);
            }
            catch (ObjectNotFoundException e) {
                // TODO: throw RevisionNotFoundException???
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // overwrites inherited
    public void removeRevisionContent
        (Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException {
        
        if (uri instanceof ResourceId) {
            super.removeRevisionContent(uri, revisionDescriptor);
        }
        else {
            try {
                ResourceId resourceId = obtainResourceId(uri);
                resourceId.getStore().removeRevisionContent(resourceId, revisionDescriptor);
            }
            catch (ObjectNotFoundException e) {
                throw new ServiceAccessException(this, e);
            }
        }
    }
    
    // ================================================================
    
    /**
     * NodeStore accessor
     */
    public NodeStore getNodeStore() {
        return nodeStore;
    }
    
    /**
     * Always returns false. Default implementation for this method
     * Stores that support binding should override this method.
     */
    public boolean useBinding() {
        return !"false".equalsIgnoreCase((String)getParameter("useBinding"));
    }
    
    private ObjectNode doRetrieveObjectNode(Uri uri)
        throws ServiceAccessException, ObjectNotFoundException  {
        
        ObjectNode result;
        if (Configuration.useBinding(this)) {
            result = doResolve(uri);
            result.setUuri(result.getUri());
            result.setUri(uri.toString()); // switch to uri
        } else {
            ResourceId resourceId = ResourceId.create(uri, uri.toString());
            result = resourceId.getStore().retrieveObject(resourceId);
            result.setUuri(result.getUri());
        }
        return result;
    }
    
    /**
     * Worker method that actually resolves the uri.
     */
    private ObjectNode doResolve(Uri uri) throws ObjectNotFoundException, ServiceAccessException {
        
        // check resolve cache first
        ResourceId resourceId = checkResolveCache(uri);
        
        if (resourceId == null) {
            UriPath uriPath = new UriPath(uri.toString());
            String[] segments = uriPath.tokens();
            String rootUriStr = uri.getScope().toString();
            if (!"/".equals(rootUriStr)) {
                rootUriStr += "/";
            }
            String currentUriStr = rootUriStr;
            Uri currentUri = new Uri(uri.getToken(), uri.getNamespace(), currentUriStr);
            ResourceId currentResourceId = ResourceId.create(currentUri, currentUriStr);
            
            for (int i = UriPath.segmentCount(rootUriStr); i < segments.length; i++) {
                // cache the resolved (parent) mapping
                cacheResolve(currentUri, currentResourceId);
                
                ObjectNode objectNode = currentResourceId.getStore().retrieveObject(currentResourceId);
                objectNode.setUri(currentUriStr);
                currentUriStr = uriPath.subUriPathString(0, i + 1);
                currentUri = new Uri(uri.getToken(), uri.getNamespace(), currentUriStr);
                
                String currentUuri = objectNode.getBindingUuri(segments[i]);
                if (currentUuri == null) {
                    throw new ObjectNotFoundException(currentUriStr);
                }
                currentResourceId = ResourceId.create(currentUri, currentUuri);
            }
            resourceId = currentResourceId;
            
            // cache resolve result
            cacheResolve(uri, resourceId);
        }
        return resourceId.getStore().retrieveObject(resourceId);
    }
    
    private void cacheResolve(Uri uri, ResourceId resourceId) {
        if (uri.getToken() != null) {
            uri.getToken().cacheResolve(uri, resourceId);
            //            System.out.println("@@@ >>> "+uri+"->"+resourceId);
        }
        //        else {
        //            System.out.println("@@@ WARNING - got Uri without token: "+uri);
        //        }
    }
    
    private ResourceId checkResolveCache(Uri uri) {
        ResourceId resourceId = null;
        if (uri.getToken() != null) {
            resourceId = uri.getToken().checkResolveCache(uri);
        }
        return resourceId;
    }
    
    private ResourceId obtainResourceId(Uri uri) throws ServiceAccessException, ObjectNotFoundException {
        ObjectNode objectNode = doRetrieveObjectNode(uri);
        ResourceId resourceId = ResourceId.create(uri, objectNode.getUuri());
        return resourceId;
    }
}


