/*
 * $Header: /home/cvspublic/jakarta-slide/src/share/org/apache/slide/macro/MacroImpl.java,v 1.56 2005/03/11 16:30:29 luetzkendorf Exp $
 * $Revision: 1.56 $
 * $Date: 2005/03/11 16:30:29 $
 *
 * ====================================================================
 *
 * 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.macro;

import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.slide.common.Domain;
import org.apache.slide.common.HelperProvider;
import org.apache.slide.common.Namespace;
import org.apache.slide.common.NamespaceConfig;
import org.apache.slide.common.PropertyName;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.common.SlideToken;
import org.apache.slide.common.Uri;
import org.apache.slide.content.Content;
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.RevisionDescriptorNotFoundException;
import org.apache.slide.event.EventDispatcher;
import org.apache.slide.event.MacroEvent;
import org.apache.slide.event.VetoException;
import org.apache.slide.lock.Lock;
import org.apache.slide.lock.NodeLock;
import org.apache.slide.lock.ObjectLockedException;
import org.apache.slide.macro.CopyRouteRedirector.CopyRoute;
import org.apache.slide.search.Search;
import org.apache.slide.security.AccessDeniedException;
import org.apache.slide.security.NodePermission;
import org.apache.slide.security.Security;
import org.apache.slide.store.Store;
import org.apache.slide.structure.ActionNode;
import org.apache.slide.structure.LinkedObjectNotFoundException;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectHasChildrenException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.structure.Structure;
import org.apache.slide.util.Configuration;

/**
 * Macro helper class.
 *
 * @version $Revision: 1.56 $
 */
public class MacroImpl implements Macro, HelperProvider {
    
    private Namespace namespace;
    private NamespaceConfig namespaceConfig;
    private Content contentHelper;
    private Structure structureHelper;
    private Security securityHelper;
    private Lock lockHelper;
    
    /**
     * Constructor.
     *
     * @param namespace Namespace
     * @param namespaceConfig Namespace configuration
     * @param contentHelper Access to content
     * @param structureHelper Access to structure
     */
    public MacroImpl(Namespace namespace, NamespaceConfig namespaceConfig,
                     Security securityHelper, Content contentHelper,
                     Structure structureHelper,
                     Lock lockHelper) {
        this.namespace       = namespace;
        this.namespaceConfig = namespaceConfig;
        this.securityHelper  = securityHelper;
        this.contentHelper   = contentHelper;
        this.structureHelper = structureHelper;
        this.lockHelper      = lockHelper;
    }
    
    /**
     * Recursive copy with overwrite macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @exception CopyMacroException Generic Slide exception
     */
    public void copy(SlideToken token, String sourceUri,
                     String destinationUri)
        throws CopyMacroException, DeleteMacroException {
        copy(token, sourceUri, destinationUri,
                 (CopyRouteRedirector)null, (CopyListener)null,
                 (DeleteTargetRedirector)null, (DeleteListener)null);
    }
    
    /**
     * Recursive copy with overwrite macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param copyRedirector  the CopyRoutRedirector may be used to redirect
     *                        the source and/or destination URI of the
     *                         <code>copy</code> operation.
     *                        (May be <code>null</code>.)
     * @param copyListener    the CopyListener that will be notified
     *                        before and after copying a resource.
     *                        (May be <code>null</code>)
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a destination
     *                        that will be overwritten by the copy.
     *                        (May be <code>null</code>)
     * @exception CopyMacroException Generic Slide exception
     */
    public void copy(SlideToken token, String sourceUri,
                     String destinationUri, CopyRouteRedirector copyRedirector, CopyListener copyListener,
                     DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws CopyMacroException, DeleteMacroException {
        
        this.copy(token, sourceUri, destinationUri,
                  RECURSIVE_OVERWRITE_PARAMETERS, copyRedirector, copyListener,
                  deleteRedirector, deleteListener);
    }
    
    
    /**
     * Copy macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param parameters Macro parameters
     * @exception CopyMacroException Generic Slide exception
     */
    public void copy(SlideToken token, String sourceUri,
                     String destinationUri, MacroParameters parameters)
        throws CopyMacroException, DeleteMacroException {
        copy(token, sourceUri, destinationUri, parameters, null, null, null, null);
    }
    
    /**
     * Copy macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param parameters Macro parameters
     * @param copyRedirector  the CopyRoutRedirector may be used to redirect
     *                        the source and/or destination URI of the
     *                         <code>copy</code> operation.
     *                        (May be <code>null</code>.)
     * @param copyListener    the CopyListener that will be notified
     *                        before and after copying a resource.
     *                        (May be <code>null</code>)
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a destination
     *                        that will be overwritten by the copy.
     *                        (May be <code>null</code>)
     * @exception CopyMacroException Generic Slide exception
     */
    public void copy(SlideToken token, String sourceUri,
                     String destinationUri, MacroParameters parameters,
                     CopyRouteRedirector copyRedirector, CopyListener copyListener,
                     DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws CopyMacroException, DeleteMacroException {
        
        Domain.debug("Copy " + sourceUri + " to " + destinationUri);
        
        // Now performing the actual copy
        CopyMacroException e = new CopyMacroException("Copy failed");
        
        //----------------
        // handle the case that source and destination are identical
        if (sourceUri.equals(destinationUri)) {
            e.addException(new ForbiddenException(sourceUri));
            throw e;
        }
        
        Uri source = namespace.getUri(token, sourceUri);
        Uri destination = namespace.getUri(token, destinationUri);
        Store store = source.getStore();

        if (store.isMacroCopySupported() && source.getStore() == destination.getStore()
                && namespace.isTreeInSingleStore(source) && namespace.isTreeInSingleStore(destination)) {
            try {
                if (Configuration.useIntegratedSecurity()) {
                    recursiveAccessCheck(source, namespaceConfig.getReadObjectAction());
                    recursiveAccessCheck(destination, namespaceConfig.getCreateObjectAction());
                }
                if (Configuration.useIntegratedLocking()) {
                    recursiveLockCheck(destination);
                }

                if (parameters.isDeleteCreate()) {
                    try {
                        destination.getStore().retrieveObject(destination);
                        delete(token, destinationUri);
                    } catch (ObjectNotFoundException one) {
                        // if it is not there, we are already done :)
                    }
                }
                
                try {
                    store.macroCopy(source, destination, parameters.isOverwrite(), parameters
                            .isRecursive());
                } catch (ObjectNotFoundException s) {
                    // this means something that should be there (the target
                    // directory to copy to)
                    // does not exist
                    throw new ConflictException(s.getObjectUri());
                }

                Uri parentUri = destination.getParentUri();
                ObjectNode parentNode = parentUri.getStore().retrieveObject(parentUri);
                
                ObjectNode destinationNode = destination.getStore().retrieveObject(destination);
                // in case we are overwriting it
                parentNode.removeChild(destinationNode);
                parentNode.addChild(destinationNode);
                parentUri.getStore().storeObject(parentUri, parentNode);

                
            } catch (AccessDeniedException x) {
                e.addException(x);
                throw e;
            } catch (ObjectNotFoundException x) {
                e.addException(x);
                throw e;
            } catch (ObjectAlreadyExistsException x) {
                e.addException(x);
                throw e;
            } catch (ServiceAccessException x) {
                e.addException(x);
                throw e;
            } catch (SlideException x) {
                e.addException(x);
                throw e;
            }
        } else {
            
            Map alreadyCopied = new HashMap(); // maps source-UURI -> destination-URI
            parameters.setParameter( ALREADY_COPIED, alreadyCopied );
            
            copyObject(token, sourceUri, destinationUri, parameters, true, e,
                       copyRedirector, copyListener, deleteRedirector, deleteListener);
            
            // If there were errors, we throw the nested exception
            if (!e.isEmpty()) {
                throw e;
            }
        }
        try {
            if ( MacroEvent.COPY.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(MacroEvent.COPY, new MacroEvent(this, token, namespace, sourceUri, destinationUri));
        } catch ( VetoException ve ) {
            throw new CopyMacroException(ve.getMessage());
        }
    }

    protected void recursiveAccessCheck(Uri uri, ActionNode action) throws SlideException,
            AccessDeniedException {
        try {
            ObjectNode node = uri.getStore().retrieveObject(uri);
            securityHelper.checkCredentials(uri.getToken(), node, action);
            Iterator i = node.getChildren().iterator();
            while (i.hasNext()) {
                String child = (String) i.next();
                Uri childUri = namespace.getUri(uri.getToken(), child);
                recursiveAccessCheck(childUri, action);
            }
        } catch (ObjectNotFoundException onfe) {
            // if it is not there it access can not be denied
        }
    }
    
    protected void recursiveLockCheck(Uri uri) throws SlideException, ObjectLockedException {
        try {
            ObjectNode node = uri.getStore().retrieveObject(uri);
            ActionNode action = namespaceConfig.getCreateObjectAction();
            lockHelper.checkLock(uri.getToken(), node, action);
            Iterator i = node.getChildren().iterator();
            while (i.hasNext()) {
                String child = (String) i.next();
                Uri childUri = namespace.getUri(uri.getToken(), child);
                recursiveLockCheck(childUri);
            }
        } catch (ObjectNotFoundException onfe) {
            // if it is not there it can not be locked
        }
    }

    // TODO: copyRedirector not used
    public void rebind(SlideToken token, String sourceUri,
                       String destinationUri, MacroParameters parameters,
                       CopyRouteRedirector copyRedirector, CopyListener copyListener,
                       DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws CopyMacroException, DeleteMacroException {
        
        CopyMacroException e = new CopyMacroException("Rebind failed");
        
        //----------------
        // handle the case that source and destination are identical
        if (sourceUri.equals(destinationUri)) {
            e.addException(new ForbiddenException(sourceUri));
            throw e;
        }
        
        Uri source = namespace.getUri(token, sourceUri);
        Uri destination = namespace.getUri(token, destinationUri);
        try {
            recursiveLockCheck(source);
            recursiveLockCheck(destination);
        } catch (SlideException ex) {
            e.addException(ex);
            throw e;
        }
        
        if (parameters.isDeleteCreate()) {
            try {
                // If the object we want to overwrite exists, we delete is first
                structureHelper.retrieve(token, destinationUri);
                delete(token, destinationUri, deleteRedirector, deleteListener);
            } catch(ObjectNotFoundException onf) {
                // Silent catch, the target doesn't exist
            } catch(DeleteMacroException s) {
                throw s;
            } catch(SlideException s) {
                e.addException(s);
                throw e;
            }
        }
        //----------------
        
        UriHandler destinationUh = new UriHandler(destinationUri);
        UriHandler destinationParentUh = destinationUh.getParent();
        ObjectNode destinationParentNode = null;
        String destinationSegment = destinationUh.getLastSegment();
        UriHandler sourceUh = new UriHandler(sourceUri);
        UriHandler sourceParentUh = sourceUh.getParent();
        ObjectNode sourceParentNode = null;
        String sourceSegment = sourceUh.getLastSegment();
        ObjectNode sourceNode = null;
        
        try {
            try {
                destinationParentNode =
                    structureHelper.retrieve(token, destinationParentUh.toString(), false );
            }
            catch (ObjectNotFoundException ex) {
                throw new ConflictException(destinationParentUh.toString());
            }
            sourceParentNode =
                structureHelper.retrieve(token, sourceParentUh.toString(), false );
            sourceNode =
                structureHelper.retrieve(token, sourceUri, false );
        }
        catch (SlideException x) {
            e.addException(x);
            throw e;
        }
        
        try {
            // notify Listeners
            if (copyListener != null) {
                copyListener.beforeCopy(sourceUri, destinationUri, true);
            }
            
            // Postcondition: DAV:lock-deleted
            Enumeration locksEnum = lockHelper.enumerateLocks(token, sourceUri, false);
            while (locksEnum.hasMoreElements()) {
                lockHelper.unlock(token, (NodeLock)locksEnum.nextElement());
            }
            
            structureHelper.addBinding( token, destinationParentNode, destinationSegment, sourceNode );
            structureHelper.removeBinding( token, sourceParentNode, sourceSegment );
        }
        catch (SlideException x) {
            e.addException(x);
            throw e;
        }
    }
    
    /**
     * Recursive move with overwrite macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @exception CopyMacroException Exception occured during copy
     * @exception DeleteMacroException Exception occured during deletion
     */
    public void move(SlideToken token, String sourceUri,
                     String destinationUri)
        throws CopyMacroException, DeleteMacroException {
        move(token, sourceUri, destinationUri,
                 (CopyRouteRedirector)null, (CopyListener)null,
                 (DeleteTargetRedirector)null, (DeleteListener)null);
    }
    
    /**
     * Recursive move with overwrite macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param copyRedirector  the CopyRoutRedirector may be used to redirect
     *                        the source and/or destination URI of the
     *                         <code>copy</code> operation.
     *                        (May be <code>null</code>.)
     * @param copyListener    the CopyListener that will be notified
     *                        before and after copying a resource.
     *                        (May be <code>null</code>)
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a resource.
     *                        (May be <code>null</code>)
     * @exception CopyMacroException Exception occured during copy
     * @exception DeleteMacroException Exception occured during deletion
     */
    public void move(SlideToken token, String sourceUri,
                     String destinationUri, CopyRouteRedirector copyRedirector, CopyListener copyListener,
                     DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws CopyMacroException, DeleteMacroException {
        
        move(token, sourceUri, destinationUri,
             RECURSIVE_OVERWRITE_PARAMETERS,
             copyRedirector, copyListener, deleteRedirector, deleteListener);
    }
    
    
    /**
     * Move macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param parameters Macro parameters
     * @exception CopyMacroException Exception occured during copy
     * @exception DeleteMacroException Exception occured during deletion
     */
    public void move(SlideToken token, String sourceUri,
                     String destinationUri, MacroParameters parameters)
        throws CopyMacroException, DeleteMacroException {
        move(token, sourceUri, destinationUri, parameters, null, null, null, null);
    }
    
    /**
     * Move macro.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param parameters Macro parameters
     * @param copyRedirector  the CopyRoutRedirector may be used to redirect
     *                        the source and/or destination URI of the
     *                         <code>copy</code> operation.
     *                        (May be <code>null</code>.)
     * @param copyListener    the CopyListener that will be notified
     *                        before and after copying a resource.
     *                        (May be <code>null</code>)
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a resource.
     *                        (May be <code>null</code>)
     * @exception CopyMacroException Exception occured during copy
     * @exception DeleteMacroException Exception occured during deletion
     */
    public void move(SlideToken token, String sourceUri,
                     String destinationUri, MacroParameters parameters,
                     CopyRouteRedirector copyRedirector, CopyListener copyListener,
                     DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws CopyMacroException, DeleteMacroException {
        
        if (Configuration.useBinding(namespace.getUri(token, sourceUri).getStore()) &&
            Configuration.useBinding(namespace.getUri(token, destinationUri).getStore()) &&
            sameStore(token, sourceUri, destinationUri)) {
            rebind(token, sourceUri, destinationUri, parameters,
                   copyRedirector, copyListener, deleteRedirector, deleteListener);
        }
        else {
            CopyMacroException e = new CopyMacroException("Move failed");
            
            Uri source = namespace.getUri(token, sourceUri);
            Uri destination = namespace.getUri(token, destinationUri);
            Store store = source.getStore();

            if (store.isMacroMoveSupported() && source.getStore() == destination.getStore()
                    && namespace.isTreeInSingleStore(source) && namespace.isTreeInSingleStore(destination)) {
                try {
                    // handle the case that source and destination are identical
                    if (sourceUri.equals(destinationUri)) {
                        e.addException(new ForbiddenException(sourceUri));
                        throw e;
                    }
                    
                    if (Configuration.useIntegratedSecurity()) {
                        recursiveAccessCheck(source, namespaceConfig.getRemoveObjectAction());
                        recursiveAccessCheck(destination, namespaceConfig.getCreateObjectAction());
                    }
                    if (Configuration.useIntegratedLocking()) {
                        recursiveLockCheck(source);
                        recursiveLockCheck(destination);
                    }

                    Uri sourceParentUri = source.getParentUri();
                    ObjectNode sourceParentNode = sourceParentUri.getStore().retrieveObject(sourceParentUri);
                    
                    ObjectNode sourceNode = source.getStore().retrieveObject(source);
                    sourceParentNode.removeChild(sourceNode);
                    sourceParentUri.getStore().storeObject(sourceParentUri, sourceParentNode);

                    try {
                        store.macroMove(source, destination, parameters.isOverwrite());
                    } catch (ObjectNotFoundException s) {
                        // this means something that should be there (the target
                        // directory to copy to)
                        // does not exist
                        throw new ConflictException(s.getObjectUri());
                    }

                    Uri parentUri = destination.getParentUri();
                    ObjectNode parentNode = parentUri.getStore().retrieveObject(parentUri);
                    
                    ObjectNode destinationNode = destination.getStore().retrieveObject(destination);
                    parentNode.removeChild(destinationNode);
                    parentNode.addChild(destinationNode);
                    parentUri.getStore().storeObject(parentUri, parentNode);

                } catch (ObjectNotFoundException x) {
                    e.addException(x);
                    throw e;
                } catch (ObjectAlreadyExistsException x) {
                    e.addException(x);
                    throw e;
                } catch (ServiceAccessException x) {
                    e.addException(x);
                    throw e;
                } catch (AccessDeniedException x) {
                    e.addException(x);
                    throw e;
                } catch (SlideException x) {
                    e.addException(x);
                    throw e;
                }
            } else {
                copy(token, sourceUri, destinationUri, parameters,
                     copyRedirector, copyListener, deleteRedirector, deleteListener);
                delete(token, sourceUri, parameters, deleteRedirector, deleteListener);
            }
        }

        try {
            if ( MacroEvent.MOVE.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(MacroEvent.MOVE, new MacroEvent(this, token, namespace, sourceUri, destinationUri));
        } catch ( VetoException ve ) {
            throw new CopyMacroException(ve.getMessage()); // FIXME: Where is the MoveMacroException?
        }
    }
    
    /**
     * Recursive delete.
     *
     * @param token Credentials token
     * @param targetUri Uri of the object to delete
     * @exception DeleteMacroException Generic Slide exception
     */
    public void delete(SlideToken token, String targetUri)
        throws DeleteMacroException {
        delete(token, targetUri, (DeleteTargetRedirector)null, (DeleteListener)null);
    }
    
    /**
     * Recursive delete.
     *
     * @param token Credentials token
     * @param targetUri Uri of the object to delete
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a resource.
     *                        (May be <code>null</code>)
     * @exception DeleteMacroException Generic Slide exception
     */
    public void delete(SlideToken token, String targetUri,
                       DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws DeleteMacroException {
        delete(token, targetUri, RECURSIVE_OVERWRITE_PARAMETERS, deleteRedirector, deleteListener);
    }
    
    
    /**
     * Delete macro.
     *
     * @param token Credentials token
     * @param targetUri Uri of the source
     * @param parameters Macro parameters, not used right now,
     * so it can be null
     * @exception DeleteMacroException Generic Slide exception
     */
    public void delete(SlideToken token, String targetUri,
                       MacroParameters parameters)
        throws DeleteMacroException {
        delete(token, targetUri, parameters, null, null);
    }
    
    /**
     * Delete macro.
     *
     * @param token Credentials token
     * @param targetUri Uri of the source
     * @param parameters Macro parameters, not used right now,
     * so it can be null
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a resource.
     *                        (May be <code>null</code>)
     * @exception DeleteMacroException Generic Slide exception
     */
    public void delete(SlideToken token, String targetUri, MacroParameters parameters,
                       DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener)
        throws DeleteMacroException {
        
        Domain.debug("Delete " + targetUri);
        
        DeleteMacroException e = new DeleteMacroException("Delete failed");
        
        Uri destination = namespace.getUri(token, targetUri);
        Store store = destination.getStore();

        if (store.isMacroDeleteSupported() && namespace.isTreeInSingleStore(destination)) {
            try {
                if (Configuration.useIntegratedSecurity()) {
                    recursiveAccessCheck(destination, namespaceConfig.getRemoveObjectAction());
                }
                if (Configuration.useIntegratedLocking()) {
                    recursiveLockCheck(destination);
                }

                Uri parentUri = destination.getParentUri();
                ObjectNode parentNode = parentUri.getStore().retrieveObject(parentUri);
                
                ObjectNode destinationNode = destination.getStore().retrieveObject(destination);
                parentNode.removeChild(destinationNode);
                parentUri.getStore().storeObject(parentUri, parentNode);

                store.macroDelete(destination);
                
            } catch (ObjectNotFoundException x) {
                e.addException(x);
                throw e;
            } catch (ServiceAccessException x) {
                e.addException(x);
                throw e;
            } catch (AccessDeniedException x) {
                e.addException(x);
                throw e;
            } catch (SlideException x) {
                e.addException(x);
                throw e;
            }
        } else {
            deleteObject(token, targetUri, e, deleteRedirector, deleteListener);
            
            // If there were errors, we throw the nested exception
            if (!e.isEmpty()) {
                throw e;
            }
        }
        try {
            if ( MacroEvent.DELETE.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(MacroEvent.DELETE, new MacroEvent(this, token, namespace, targetUri));
        } catch ( VetoException ve ) {
            throw new DeleteMacroException(ve.getMessage());
        }
    }
    
    // -------------------------------------------------------- Private Methods
    
    
    /**
     * Copy object.
     *
     * @param token Credentials token
     * @param sourceUri Uri of the source
     * @param destinationUri Uri of the destination
     * @param parameters Macro parameters
     * @param copyRedirector  the CopyRoutRedirector may be used to redirect
     *                        the source and/or destination URI of the
     *                         <code>copy</code> operation.
     *                        (May be <code>null</code>.)
     * @param copyListener    the CopyListener that will be notified
     *                        before and after copying a resource.
     *                        (May be <code>null</code>)
     * @param CopyMacroException Exception occured during copy
     */
    private void copyObject(SlideToken token, String sourceUri,
                            String destinationUri, MacroParameters parameters, boolean isRootOfCopy,
                            CopyMacroException e, CopyRouteRedirector copyRedirector, CopyListener copyListener,
                            DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener
                           ) {
        
        Domain.debug("Copy object : from " + sourceUri + " to "
                         + destinationUri);
        
        try {
            // check for lock-null
            if (isLockNull(token, sourceUri)) {
                // skip
                return;
            }
            
            // Remember children of orginal source
            ObjectNode sourceNode =
                structureHelper.retrieve(token, sourceUri, false);
            Enumeration sourceNodeChildren = sourceNode.enumerateChildren();
            ObjectNode destinationNode = null;
            
            // now let the client redirect
            if (copyRedirector != null) {  // TODO: dump re-directing
                CopyRoute copyRoute = new CopyRoute(sourceUri, destinationUri);
                copyRoute = copyRedirector.getRedirectedCopyRoute(copyRoute);
                sourceUri = copyRoute.getSourceUri();
                destinationUri = copyRoute.getDestinationUri();
                sourceNode = structureHelper.retrieve(token, sourceUri, false);
                // note that childrenList is *not* re-assigned. This might be a bug ...
            }
            
            // notify CopyListener
            if (copyListener != null) {
                copyListener.beforeCopy(sourceUri, destinationUri, isRootOfCopy );
            }
            
            // delete target if it is the root of the copied tree
            if (isRootOfCopy && parameters.isDeleteCreate()) {
                try {
                    // We make sure the object we want to overwrite exists
                    structureHelper.retrieve(token, destinationUri);
                    if (parameters.getParameter(PARENT_BINDINGS) != null) {
                        Map parentBindings = (Map)parameters.getParameter(PARENT_BINDINGS);
                        Iterator i = parentBindings.entrySet().iterator();
                        while (i.hasNext()) {
                            Map.Entry me = (Map.Entry)i.next();
                            String uriToDelete = (String)me.getKey()+"/"+(String)me.getValue();
                            delete(token, uriToDelete, deleteRedirector, deleteListener);
                        }
                    }
                    else {
                        delete(token, destinationUri, deleteRedirector, deleteListener);
                    }
                } catch(ObjectNotFoundException onf) {
                    // Silent catch, the target doesn't exist
                } catch(DeleteMacroException s) {
                    Enumeration en = s.enumerateExceptions();
                    if (en.hasMoreElements()) {
                        throw (SlideException)en.nextElement();
                    }
                }
            }
            
            boolean destinationExists = destinationExists(token, destinationUri);
            
            // Creating the copy
            if (parameters.isDeleteCreate() || !destinationExists) {
                try {
                    Map alreadyCopied = (Map)parameters.getParameter( ALREADY_COPIED );
                    
                    if (alreadyCopied.containsKey(sourceNode.getUuri())) {
                        // If a COPY request would cause a new resource to be created
                        // as a copy of an existing resource, and that COPY request
                        // has already created a copy of that existing resource,
                        // the COPY request instead creates another binding to the
                        // previous copy, instead of creating a new resource.
                        UriHandler destinationUh = new UriHandler(destinationUri);
                        UriHandler destinationParentUh = destinationUh.getParent();
                        String segment = destinationUh.getLastSegment();
                        destinationNode = structureHelper.retrieve( token, (String)alreadyCopied.get(sourceNode.getUuri()) );
                        ObjectNode destinationParentNode = structureHelper.retrieve( token, destinationParentUh.toString() );
                        structureHelper.addBinding( token, destinationParentNode, segment, destinationNode );
                    }
                    else {
                        structureHelper.create(token, sourceNode.copyObject(),
                                               destinationUri);
                        destinationNode = structureHelper.retrieve( token, destinationUri );
                        alreadyCopied.put( sourceNode.getUuri(), destinationNode.getUri() );
                    }
                } catch (ObjectNotFoundException s){
                    throw new ConflictException(s.getObjectUri());
                }
            }
            
            // Now copying revision descriptors and content
            NodeRevisionDescriptors sourceNrds =
                contentHelper.retrieve(token, sourceNode.getUri());
            if (sourceNrds.hasRevisions()) {
                NodeRevisionDescriptor sourceNrd =
                    contentHelper.retrieve(token, sourceNrds);
                NodeRevisionContent sourceNrc = null;
                if (!sourceNrd.propertyValueContains(PropertyName.RESOURCE_TYPE, "collection")) {
                    sourceNrc =
                        contentHelper.retrieve(token, sourceNrds, sourceNrd);
                }
                
                if (parameters.isDeleteCreate() || !destinationExists) {
                    contentHelper.create(token, destinationUri, sourceNrd, sourceNrc);
                }
                else {
                    NodeRevisionDescriptor destinationNrd =
                        contentHelper.retrieve(token,
                                               contentHelper.retrieve(token, destinationUri));
                    Enumeration sourceProps = sourceNrd.enumerateProperties();
                    while (sourceProps.hasMoreElements()) {
                        destinationNrd.setProperty( (NodeProperty)sourceProps.nextElement() );
                    }
                    contentHelper.store(token, destinationUri, destinationNrd, sourceNrc);
                }
            }
            
            // notify CopyListener
            if (copyListener != null) {
                copyListener.afterCopy(sourceUri, destinationUri, isRootOfCopy, destinationExists);
            }
            
            // We copy each of this object's children
            if (parameters.isRecursive()) {
                while(sourceNodeChildren.hasMoreElements()) {
                    String childUri = (String) sourceNodeChildren.nextElement();
                    String childDestinationUri = destinationUri + childUri
                        .substring(sourceNode.getUri().length());
                    copyObject(token, childUri, childDestinationUri,
                               parameters, false, e, copyRedirector, copyListener,
                               deleteRedirector, deleteListener
                              );
                }
            }
            
            // Trying to recreate permissions
            try {
                
                Enumeration sourcePermissions = securityHelper
                    .enumeratePermissions(token, sourceNode);
                
                while (sourcePermissions.hasMoreElements()) {
                    NodePermission permission =
                        (NodePermission) sourcePermissions.nextElement();
                    NodePermission newPermission =
                        new NodePermission(destinationUri,
                                           permission.getSubjectUri(),
                                           permission.getActionUri(),
                                           permission.isInheritable(),
                                           permission.isNegative());
                    securityHelper.grantPermission(token, newPermission);
                }
                
            } catch (AccessDeniedException ex) {
                // Means that we don't have modifyPermissions rights
                // (root access) on the target.
                // The copy should definitely succeed anyway,
                // so we silently catch the exception.
            }
              
        } catch(SlideException ex) {
            // ex.printStackTrace(); //the exception is packed and
            // (hopefully) reported in the response
            e.addException(ex);
        }
        
    }
    
    private boolean destinationExists(SlideToken token, String destinationUri) throws ServiceAccessException, LinkedObjectNotFoundException, RevisionDescriptorNotFoundException, ObjectLockedException, AccessDeniedException, VetoException {
        boolean destinationExists = true;
        try {
            contentHelper.retrieve(token, contentHelper.retrieve(token,
                    destinationUri));
        }
        catch (ObjectNotFoundException x) {
            destinationExists = false;
        }
        return destinationExists;
    }
    
    
    /**
     * Delete object function. Recursive for now.
     *
     * @param token Credentials token
     * @param targetUri Uri of the source
     * @param parameters Macro parameters, not used right now, so it can
     * be null
     * @param e Nested exception
     * @param deleteRedirector  the DeleteTargetRedirector may be used to redirect
     *                          the <code>delete</code> operation to a different
     *                          target. (May be <code>null</code>.)
     * @param deleteListener  the DeleteListener that will be notified
     *                        before and after deleting a resource.
     *                        (May be <code>null</code>)
     */
    private void deleteObject(SlideToken token, String targetUri, MacroException e,
                              DeleteTargetRedirector deleteRedirector, DeleteListener deleteListener) {
        
        Domain.debug("Delete object : " + targetUri);
        
        try {
            ObjectNode currentObject =
                structureHelper.retrieve(token, targetUri, false);
            
            if (!Configuration.useBinding(namespace.getUri(token, targetUri).getStore()) || currentObject.numberOfParentBindings() < 2) {
                // Removing children objects
                if (currentObject.hasChildren()) {
                    Enumeration children = currentObject.enumerateChildren();
                    while (children.hasMoreElements()) {
                        String childUri = (String)children.nextElement();
                        deleteObject(token, childUri, e, deleteRedirector, deleteListener);
                    }
                }

                // Removing links objects
                if (currentObject.hasLinks()) {
                    Enumeration links = currentObject.enumerateLinks();
                    while (links.hasMoreElements()) {
                        String linkUri = (String)links.nextElement();
                        deleteObject(token, linkUri, e, deleteRedirector, deleteListener);
                    }
                }
                
                
                // now let the client redirect
                if (deleteRedirector != null) {
                    targetUri = deleteRedirector.getRedirectedTargetUri(targetUri);
                    currentObject =  structureHelper.retrieve(token, targetUri, false);
                }
                
                // notify DeleteListener
                if (deleteListener != null) {
                    deleteListener.beforeDelete(targetUri);
                }
                
                NodeRevisionDescriptors revisionDescriptors =
                    contentHelper.retrieve(token, currentObject.getUri());
               
                // remove the associated locks
                Enumeration locks = lockHelper.enumerateLocks
                    (token, currentObject.getUri(), false);
                while (locks.hasMoreElements()) {
                    lockHelper.unlock(token, (NodeLock) locks.nextElement());
                }
                // remove the associated security
                Enumeration permissions = securityHelper.enumeratePermissions
                    (token, currentObject);
                while (permissions.hasMoreElements()) {
                    NodePermission permission =
                        (NodePermission) permissions.nextElement();
                    securityHelper.revokePermission(token, permission);
                }
                
                // remove all revisions
                if( !revisionDescriptors.isRedirected() ) {
                    Enumeration revisionNumberEnum =
                        revisionDescriptors.enumerateRevisionNumbers();
                    if (revisionNumberEnum != null) {
                        while (revisionNumberEnum.hasMoreElements()) {
                            contentHelper.remove
                                (token, currentObject.getUri(),
                                     (NodeRevisionNumber)revisionNumberEnum.nextElement());
                        }
                    }
                    removeHiddenRevisions( token, targetUri );
                    
                    // remove the NodeRevisionDescriptors object
                    contentHelper.remove(token, revisionDescriptors);
                }
            }
            
            // Removing object.
            structureHelper.remove(token, currentObject);
            
            // notify DeleteListener
            if (deleteListener != null) {
                deleteListener.afterDelete(targetUri);
            }
            
        } catch (ObjectHasChildrenException ex) {
            // ignore, the object has children because something else failed
            // (one of the children is locked, for example), and the
            // corresponding exception has already been added the the
            // MacroException
        } catch (SlideException ex) {
            e.addException(ex);
        }
        
    }
    
    /**
     * Removes all <i>hidden</i> revisions for the specified URI. Currently there are hidden
     * revisions only at 0.0.
     */
    private void removeHiddenRevisions( SlideToken token, String targetUri ) throws ServiceAccessException, ObjectNotFoundException, LinkedObjectNotFoundException, AccessDeniedException, ObjectLockedException, VetoException {
        
        // Can this piece of code be moved to MoveMethod.afterDelete() and DeleteMethod.afterDelete?
        // It would look more consistent (the copying of hidden revisions is there!!)
        // BUT: I dont think it's a good idea because the deleteObject() method would delete
        // the NRDS while hidden revisions remain.
        
        try {
            contentHelper.remove(token, targetUri, NodeRevisionNumber.HIDDEN_0_0 );
        }
        catch (RevisionDescriptorNotFoundException ne) {
            // ignore
        }
    }
    
    /**
     ** Uri handler.
     **
     **/
    private static class UriHandler {
        
        /** The path tokens */
        String[] tokens = null;
        final String uriDelimiter = "/";
        
        /**
         ** Default constructor.
         **/
        UriHandler( String uri ) {
            StringTokenizer ut = new StringTokenizer( uri, uriDelimiter );
            int ntok = ut.countTokens();
            this.tokens = new String[ntok];
            for( int i = 0; i < ntok; i++ )
                tokens[i] = ut.nextToken();
        }
        
        /**
         ** Default constructor.
         **/
        UriHandler( String[] toks ) {
            this.tokens = new String[toks.length];
            for( int i = 0; i < toks.length; i++ )
                tokens[i] = toks[i];
        }
        
        /**
         ** Default constructor.
         ** @pre number <= toks.length
         **/
        UriHandler( String[] toks, int number ) {
            this.tokens = new String[number];
            for( int i = 0; i < number; i++ )
                tokens[i] = toks[i];
        }
        
        /**
         ** Check whether this is the root.
         **/
        boolean isRoot() {
            return (tokens.length == 0);
        }
        
        /**
         * Return the parent.
         * Example: for /a/b/c returns: /a/b
         */
        UriHandler getParent() {
            if( isRoot() )
                return null;
            return new UriHandler( tokens, tokens.length - 1 );
        }
        
        /**
         * Checks this URI is parent of the specified URI
         */
        boolean isParentOf( UriHandler uh ) {
            return equals( uh.getParent() );
        }
        
        /**
         * Returns the node name (last token), e.g. "c" for /a/b/c
         */
        String getLastSegment() {
            return tokens[tokens.length - 1];
        }
        
        /**
         *
         */
        public boolean equals( Object o ) {
            if( o instanceof UriHandler ) {
                UriHandler ouh = (UriHandler)o;
                return Arrays.equals( tokens, ouh.tokens );
            }
            return false;
        }
        
        /**
         *
         */
        public int hashCode() {
            return tokens.length;
        }
        
        /**
         * Return string representation.
         */
        public String toString() {
            StringBuffer b = new StringBuffer();
            if( tokens.length == 0 )
                b.append( uriDelimiter );
            for( int i = 0; i < tokens.length; i++ )
                b.append( uriDelimiter ).append( tokens[i] );
            return b.toString();
        }
    }
    
    private boolean sameStore(SlideToken token, String leftUri, String rightUri) {
        Store leftStore = namespace.getUri(token, leftUri).getStore();
        Store rightStore = namespace.getUri(token, rightUri).getStore();
        if (leftStore == null || rightStore == null) {
            throw new IllegalStateException("Got null store: leftStore="+leftStore+", rightStore="+rightStore);
        }
        return leftStore == rightStore;
    }
    
    private boolean isLockNull( SlideToken slideToken, String uriStr ) throws ServiceAccessException {
        boolean isLockNull = false;
        try {
            NodeRevisionDescriptor nrd =
                contentHelper.retrieve(slideToken, contentHelper.retrieve(slideToken, uriStr));
            isLockNull = isLockNull( nrd );
        }
        catch (ServiceAccessException x) {
            throw x;
        }
        catch (SlideException x) {
            // ignore silently
        }
        return isLockNull;
    }
    
    private boolean isLockNull( NodeRevisionDescriptor nrd ) {
        return nrd.propertyValueContains("resourcetype", "lock-null");
    }
    
    
    public Content getContentHelper() {
        return this.contentHelper;
    }
    public Lock getLockHelper() {
        return this.lockHelper;
    }
    public Macro getMacroHelper() {
        return this;
    }
    public Search getSearchHelper() {
        return null;
    }
    public Security getSecurityHelper() {
        return securityHelper;
    }
    public Structure getStructureHelper() {
        return structureHelper;
    }
}



