/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2009, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.system.server.profileservice.repository.clustered.local;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.jboss.logging.Logger;
import org.jboss.profileservice.spi.DeploymentRepository;
import org.jboss.profileservice.spi.ProfileKey;
import org.jboss.system.server.profileservice.repository.clustered.metadata.RepositoryContentMetadata;
import org.jboss.system.server.profileservice.repository.clustered.metadata.RepositoryItemMetadata;
import org.jboss.system.server.profileservice.repository.clustered.metadata.RepositoryRootMetadata;
import org.jboss.system.server.profileservice.repository.clustered.sync.ContentModification;
import org.jboss.system.server.profileservice.repository.clustered.sync.NoOpSynchronizationAction;
import org.jboss.system.server.profileservice.repository.clustered.sync.RemovalMetadataInsertionAction;
import org.jboss.system.server.profileservice.repository.clustered.sync.SynchronizationAction;
import org.jboss.system.server.profileservice.repository.clustered.sync.SynchronizationActionContext;
import org.jboss.system.server.profileservice.repository.clustered.sync.SynchronizationId;
import org.jboss.system.server.profileservice.repository.clustered.sync.TwoPhaseCommitAction;
import org.jboss.virtual.VFS;
import org.jboss.virtual.VirtualFile;

/**
 * Abstract base class for a {@link LocalContentManager} implementation.
 *
 * @author Brian Stansberry
 * 
 * @version $Revision: $
 */
public abstract class AbstractLocalContentManager<T extends SynchronizationActionContext> implements LocalContentManager<T>
{
   private final Logger log = Logger.getLogger(getClass());
   
   private RepositoryContentMetadata officialContentMetadata;
   private RepositoryContentMetadata currentContentMetadata;
   private final Map<String, URI> namedURIMap;
   private final Map<String, VirtualFile> vfCache = new ConcurrentHashMap<String, VirtualFile>();
   private final ProfileKey profileKey;
   private final String storeName;
   private final String localNodeName;
   private final ContentMetadataPersister contentMetadataPersister;
   private List<TwoPhaseCommitAction<T>> currentSynchronizationActions;
   private T currentSynchronizationActionContext;
   private Map<RepositoryItemMetadata, InputStream> pendingStreams = new ConcurrentHashMap<RepositoryItemMetadata, InputStream>();

   
   protected static String createStoreName(ProfileKey key)
   {
      // Normal case
      if (ProfileKey.DEFAULT.equals(key.getDomain()) 
            && ProfileKey.DEFAULT.equals(key.getServer()))
      {
         return key.getName();
      }
      
      StringBuilder sb = new StringBuilder();
      if (ProfileKey.DEFAULT.equals(key.getDomain()) == false)
      {
         sb.append(key.getDomain());
         sb.append('-');
      }
      if (ProfileKey.DEFAULT.equals(key.getServer()) == false)
      {
         sb.append(key.getServer());
         sb.append('-');
      }
      sb.append(key.getName());
      return sb.toString();
   }
   
   /**
    * Create a new AbstractLocalContentManager.
    * 
    * @param namedURIMap Map of URIs managed by this object, keyed by a
    *                    String identifier
    * @param profileKey  key of the profile the content of which this
    *                    object is managing
    * @param localNodeName name of the local node in the cluster
    * @param contentMetadataPersister object to use for storing/retrieving content metadata
    */
   protected AbstractLocalContentManager(Map<String, URI> namedURIMap,
         ProfileKey profileKey, String localNodeName, 
         ContentMetadataPersister contentMetadataPersister)
   {
      if (namedURIMap == null)
      {
         throw new IllegalArgumentException("Null namedURIMap");
      }
      if (profileKey == null)
      {
         throw new IllegalArgumentException("Null profileKey");
      }
      if (localNodeName == null)
      {
         throw new IllegalArgumentException("Null localNodeName");
      }
      if (contentMetadataPersister == null)
      {
         throw new IllegalArgumentException("Null contentMetadataPersister");
      }
      
      this.namedURIMap = namedURIMap;
      this.profileKey = profileKey;
      this.storeName = createStoreName(profileKey);
      this.localNodeName = localNodeName;
      this.contentMetadataPersister = contentMetadataPersister;
      this.officialContentMetadata = this.contentMetadataPersister.load(this.storeName);
   }
   
   // -------------------------------------------------------------  Properties

   public String getLocalNodeName()
   {
      return localNodeName;
   }
   
   public String getStoreName()
   {
      return storeName;
   }
   
   // ----------------------------------------------------  LocalContentManager

   public RepositoryContentMetadata getOfficialContentMetadata()
   {
      return officialContentMetadata;
   }

   public RepositoryContentMetadata createEmptyContentMetadata()
   {
      RepositoryContentMetadata md = new RepositoryContentMetadata(profileKey);
      List<RepositoryRootMetadata> roots = new ArrayList<RepositoryRootMetadata>();
      for (Map.Entry<String, URI> entry : namedURIMap.entrySet())
      {
         RepositoryRootMetadata rmd = new RepositoryRootMetadata();
         rmd.setName(entry.getKey());
         roots.add(rmd);
      }
      md.setRepositories(roots);
      return md;      
   }

   public RepositoryContentMetadata getCurrentContentMetadata() throws IOException
   {
      synchronized (this)
      {
         RepositoryContentMetadata md = new RepositoryContentMetadata(profileKey);
         List<RepositoryRootMetadata> roots = new ArrayList<RepositoryRootMetadata>();
         RepositoryContentMetadata official = getOfficialContentMetadata();
         for (Map.Entry<String, URI> entry : namedURIMap.entrySet())
         {
            RepositoryRootMetadata rmd = new RepositoryRootMetadata();
            rmd.setName(entry.getKey());
            RepositoryRootMetadata existingRmd = official == null ? null : official.getRepositoryRootMetadata(entry.getKey());
            
            VirtualFile root = getCachedVirtualFile(entry.getValue());         
            if (isDirectory(root))
            {
               for(VirtualFile child: root.getChildren())
               {
                  createItemMetadataFromScan(rmd, existingRmd, child, root);
               }
            }
            else
            {
               // The root is itself an item. Treat it as a "child" of
               // itself with no relative path
               createItemMetadataFromScan(rmd, existingRmd, root, root);
            }
            
            roots.add(rmd);
         }
         md.setRepositories(roots);
         
         // Retain any existing "removed item" metadata -- but only if
         // it wasn't re-added!!
         RepositoryContentMetadata existing = getOfficialContentMetadata();
         if (existing != null)
         {
            for (RepositoryRootMetadata existingRoot : existing.getRepositories())
            {
               RepositoryRootMetadata rmd = md.getRepositoryRootMetadata(existingRoot.getName());
               if (rmd != null)
               {
                  Collection<RepositoryItemMetadata> rimds = rmd.getContent();
                  for (RepositoryItemMetadata existingItem : existingRoot.getContent())
                  {
                     if (existingItem.isRemoved() // but check for re-add 
                           && rmd.getItemMetadata(existingItem.getRelativePathElements()) == null)
                     {
                        rimds.add(new RepositoryItemMetadata(existingItem));
                     }
                  }
               }
            }
         }
         
         this.currentContentMetadata = md;
         return this.currentContentMetadata;
      }
   }

   public List<? extends SynchronizationAction<T>> initiateSynchronization(SynchronizationId<?> id,
         List<ContentModification> modifications, RepositoryContentMetadata toInstall, boolean localLed)
   {
      if (id == null)
      {
         throw new IllegalArgumentException("Null id");
      }
      if (modifications == null)
      {
         throw new IllegalArgumentException("Null modifications");
      }
      if (toInstall == null)
      {
         throw new IllegalArgumentException("Null toInstall");
      }
      
      synchronized (this)
      {
         if (currentSynchronizationActionContext != null)
         {
            throw new IllegalStateException("Synchronization " + currentSynchronizationActionContext.getId() + 
                  " is already in progress");
         }
         
         this.currentSynchronizationActionContext = createSynchronizationActionContext(id, toInstall);
      }

      List<TwoPhaseCommitAction<T>> actions = new ArrayList<TwoPhaseCommitAction<T>>();
      for (ContentModification mod : modifications)
      {
         TwoPhaseCommitAction<T> action = null;
         switch (mod.getType())
         {
            case PULL_FROM_CLUSTER:
               action = createPullFromClusterAction(mod, localLed);
               break;
            case PUSH_TO_CLUSTER:
               InputStream stream = pendingStreams.remove(mod.getItem());
               if (stream == null)
               {
                  action = createPushToClusterAction(mod, localLed);
               }
               else
               {
                  action = createPushStreamToClusterAction(mod, stream);
               }
               break;
            case REMOVE_FROM_CLUSTER:
               action = createRemoveFromClusterAction(mod, localLed);
               break;
            case REMOVE_TO_CLUSTER:
               action = createRemoveToClusterAction(mod, localLed);
               break;
            case PREPARE_RMDIR_FROM_CLUSTER:
               action = createPrepareRmdirFromClusterAction(mod, localLed);
               break;
            case PREPARE_RMDIR_TO_CLUSTER:
               action = createPrepareRmdirToClusterAction(mod, localLed);
               break;
            case DIR_TIMESTAMP_MISMATCH:
               action = createDirectoryTimestampMismatchAction(mod, localLed);
               break;
            case MKDIR_FROM_CLUSTER:
               action = createMkdirFromClusterAction(mod, localLed);
               break;
            case MKDIR_TO_CLUSTER:
               action = createMkdirToClusterAction(mod, localLed);
               break;
            case REMOVAL_METADATA_FROM_CLUSTER:
               action = createRemovalMetadataAction(mod, localLed);
               break;
            default:
               throw new IllegalStateException("Unknown " + ContentModification.Type.class.getSimpleName() + " " + mod.getType());
         }
         actions.add(action);         
      }
      
      this.currentSynchronizationActions = actions;
      return Collections.unmodifiableList(actions);
   }

   public boolean prepareSynchronization(SynchronizationId<?> id)
   {
      validateSynchronization(id);
      for (TwoPhaseCommitAction<T> action : this.currentSynchronizationActions)
      {
         if (action.prepare() == false)
         {            
            if (log.isTraceEnabled())
            {
               ContentModification mod = action.getRepositoryContentModification();
               log.trace("prepare failed for " + mod.getType() + " " + mod.getItem().getRelativePath());
            }
            return false;
         }
      }
      
      if (log.isTraceEnabled())
      {
         log.trace("prepared synchronization " + id);
      }
      return true;
   }
   
   public void commitSynchronization(SynchronizationId<?> id)
   {
      validateSynchronization(id);
      for (TwoPhaseCommitAction<T> action : this.currentSynchronizationActions)
      {
         action.commit();
      }
      updateContentMetadata(this.currentSynchronizationActionContext.getInProgressMetadata());
      synchronized (this)
      {
         this.currentSynchronizationActions = null;
         this.currentSynchronizationActionContext = null;
      }
      
      if (log.isTraceEnabled())
      {
         log.trace("committed synchronization " + id);
      }
   }

   public void rollbackSynchronization(SynchronizationId<?> id)
   {
      validateSynchronization(id);
      
      for (TwoPhaseCommitAction<T> action : this.currentSynchronizationActions)
      {
         action.rollback();
      }
      synchronized (this)
      {
         this.currentSynchronizationActionContext = null;
         this.currentSynchronizationActions = null;
      }
      
      if (log.isTraceEnabled())
      {
         log.trace("rolled back synchronization " + id);
      }
   }
   
   public void installCurrentContentMetadata()
   {
      synchronized (this)
      {
         if (this.currentContentMetadata == null)
         {
            throw new IllegalStateException("No currentContentMetadata");
         }
         if (this.currentSynchronizationActionContext != null)
         {
            throw new IllegalStateException("Cannot install currentContentMetadata; " +
            		"cluster synchronization " + this.currentSynchronizationActionContext.getId() + 
            		" is in progress");
         }
         
         updateContentMetadata(this.currentContentMetadata);
      }      
   }

   public RepositoryItemMetadata getItemForAddition(String vfsPath) throws IOException
   {
      RepositoryItemMetadata item = new RepositoryItemMetadata();
      item.setRelativePath(vfsPath);
      List<String> pathElements = item.getRelativePathElements();
      String rootName = null;
      for (RepositoryRootMetadata rmd : getOfficialContentMetadata().getRepositories())
      {
         if (rmd.getItemMetadata(pathElements) != null)
         {
            // Exact match to existing item -- done
            rootName = rmd.getName();
            break;
         }
         else if (rootName == null)
         {
            // Use the first root that can accept children
            URI rootURI = namedURIMap.get(rmd.getName());
            VirtualFile vf = getCachedVirtualFile(rootURI);
            if (isDirectory(vf))
            {
               rootName = rmd.getName();
               break;               
            }
         }
      }
      
      if (rootName == null)
      {
         throw new IllegalStateException("No roots can accept children");
      }
      
      item.setRootName(rootName);
      return item;
   }

   public RepositoryContentMetadata getContentMetadataForAdd(RepositoryItemMetadata toAdd, InputStream contentIS) throws IOException
   {
      RepositoryContentMetadata result = new RepositoryContentMetadata(getOfficialContentMetadata());
      RepositoryRootMetadata rmd = result.getRepositoryRootMetadata(toAdd.getRootName());
      if (rmd == null)
      {
         throw new IllegalArgumentException("Unknown root name " + toAdd.getRootName());
      }
      RepositoryItemMetadata remove = rmd.getItemMetadata(toAdd.getRelativePathElements());
      if (remove != null)
      {
         Collection<RepositoryItemMetadata> content = rmd.getContent();
         if (remove.isDirectory())
         {
            for (Iterator<RepositoryItemMetadata> it = content.iterator(); it.hasNext(); )
            {
               if (it.next().isChildOf(remove))
               {
                  it.remove();
               }
            }
         }
         content.remove(remove);
      }
      rmd.getContent().add(toAdd);
      pendingStreams.put(toAdd, contentIS);      
      return result;
   }

   public VirtualFile getVirtualFileForItem(RepositoryItemMetadata item) throws IOException
   {
      URI uri = namedURIMap.get(item.getRootName());
      VirtualFile vf = getCachedVirtualFile(uri);
      VirtualFile parent = null;
      List<String> path = item.getRelativePathElements();
      for (String element : path)
      {
         parent = vf;
         vf = parent.getChild(element);
         if (vf == null)
         {
            throw new IllegalStateException("No child " + element + " under " + parent);
         }
      }
      return vf;
   }
   
   public RepositoryContentMetadata getContentMetadataForRemove(VirtualFile vf) throws IOException
   {
      List<String> path = null;
      RepositoryRootMetadata root = null;
      RepositoryContentMetadata cmd = new RepositoryContentMetadata(getOfficialContentMetadata());
      for (RepositoryRootMetadata rmd : cmd.getRepositories())
      {
         URI uri = namedURIMap.get(rmd.getName());
         VirtualFile vfRoot = getCachedVirtualFile(uri);
         try
         {
            path = getRelativePath(vf, vfRoot);
            root = rmd;
            break;
         }
         catch (IllegalStateException ise)
         {
            // vf wasn't a child; ignore and move on to next root
         }
      }
      
      if (root == null)
      {
         throw new IllegalArgumentException(vf + " is not a child of any known roots");
      }
      
      RepositoryItemMetadata remove = root.getItemMetadata(path);      
      if (remove != null)
      {
         Collection<RepositoryItemMetadata> items = root.getContent();
         if (isDirectory(vf))
         {
            for (Iterator<RepositoryItemMetadata> it = items.iterator(); it.hasNext(); )
            {
               if (it.next().isChildOf(remove))
               {
                  it.remove();
               }
            }
            
         }
         items.remove(remove);
      }
      return cmd;
   }  
   
   
   // --------------------------------------------------------------  Protected


   /** 
    * Create a {@link SynchronizationActionContext} for the given cluster-wide
    * content synchronization.
    * 
    * @param id the id of the synchronization
    * @param toUpdate metadata object that should be updated as synchronization
    *                 actions are performed.
    */
   protected abstract T createSynchronizationActionContext(SynchronizationId<?> id, RepositoryContentMetadata toUpdate);
   
   /**
    * Create an action to handle the local end of a node pulling content from
    * the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createPullFromClusterAction(ContentModification mod, boolean localLed);
   
   /**
    * Create an action to handle the local end of a node pushing content to
    * the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createPushToClusterAction(ContentModification mod, boolean localLed);

   /**
    * Create an action to handle the local end of a node pushing content that is
    * read from an external-to-the-repository stream to the cluster. Used to
    * handle installation of content to the repository via 
    * {@link DeploymentRepository#addDeployment(String, org.jboss.profileservice.spi.ProfileDeployment)}. 
    * <p> 
    * This is only invoked on the node that is driving the synchronization process.
    * </p>
    * 
    * @param mod object describing the content modification this action is
    *            part of
    *            
    * @param stream the stream from which content will be read.
    *                 
    * @return an action that will handle both the local end of pushing the stream content to
    *         other nodes in the cluster <b>and</b> storing the stream content
    *         in this node's repository. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createPushStreamToClusterAction(ContentModification mod, InputStream stream);
   
   /**
    * Create an action to handle the local end of a node removing content that
    * the rest of the cluster regards as invalid.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createRemoveFromClusterAction(ContentModification mod, boolean localLed);
   
   /**
    * Create an action to handle the local end of a node removing content from
    * the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createRemoveToClusterAction(ContentModification mod, boolean localLed);

   /**
    * Create an action to handle the local end of a node removing a directory
    * from the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createPrepareRmdirToClusterAction(ContentModification mod, boolean localLed);


   protected abstract TwoPhaseCommitAction<T> createPrepareRmdirFromClusterAction(ContentModification mod, boolean localLed);
   
   /**
    * Create an action to handle the local end of a node adding a directory
    * to the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createMkdirToClusterAction(ContentModification mod,
         boolean localLed);

   /**
    * Create an action to handle the local end of a node adding a directory
    * due to its presence on the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createMkdirFromClusterAction(
         ContentModification mod, boolean localLed);

   /**
    * Create an action to handle the local end of a node updating a directory
    * timestamp to match the cluster.
    * 
    * @param mod object describing the content modification this action is
    *            part of
    * @param localLed <code>true</code> if this node is driving the synchronization
    *                 process the action is part of; <code>false</code> if 
    *                 another node is
    *                 
    * @return the action. Will not return <code>null</code>.
    */
   protected abstract TwoPhaseCommitAction<T> createDirectoryTimestampMismatchAction(
         ContentModification mod, boolean localLed);

   /**
    * Gets the current {@link SynchronizationActionContext}.
    * 
    * @return the current context, or <code>null</code> if there isn't one
    */
   protected T getSynchronizationActionContext()
   {
      return currentSynchronizationActionContext;
   }
   
   /**
    * Gets a {@link VirtualFile} corresponding to the given URI.
    * 
    * @param uri the uri. Cannot be <code>null</code>.
    * @return the virtual file
    * 
    * @throws IOException
    * @throws NullPointerException if <code>uri</code> is <code>null</code>. 
    */
   protected VirtualFile getCachedVirtualFile(URI uri) throws IOException 
   {
      VirtualFile vf = this.vfCache.get(uri.toString());
      if(vf == null)
      {
         vf = VFS.getRoot(uri);
         this.vfCache.put(uri.toString(), vf);
      }
      return vf;
   }
   
   /**
    * Gets the URI of the repository root with which the given modification
    * is associated.
    * 
    * @param mod the modification. Cannot be <code>null</code>
    * @return the URI. May be <code>null</code> if the modification is for
    *         an unknown root
    *         
    * @throws NullPointerException if <code>uri</code> is <code>null</code>.
    * 
    *  @see ContentModification#getRootName()
    */
   protected URI getRootURIForModification(ContentModification mod)
   {
      return namedURIMap.get(mod.getRootName());
   }
   
   // --------------------------------------------------------------  Private
   
   private TwoPhaseCommitAction<T> createRemovalMetadataAction(ContentModification mod,
                                                            boolean localLed)
   {
      if (localLed)
      {
         return new RemovalMetadataInsertionAction<T>(getSynchronizationActionContext(), mod);
      }
      else
      {
         return new NoOpSynchronizationAction<T>(getSynchronizationActionContext(), mod);
      }
   }
   
   private void updateContentMetadata(RepositoryContentMetadata newOfficial)
   {
      if (newOfficial.equals(this.officialContentMetadata) == false)
      {
         try
         {
            this.contentMetadataPersister.store(this.storeName, newOfficial);
         }
         catch (Exception e)
         {
            log.error("Caught exception persisting " + RepositoryContentMetadata.class.getSimpleName(), e);
         }
         this.officialContentMetadata = newOfficial;
         
         if (log.isTraceEnabled())
         {
            log.trace("updateContentMetadata(): updated official metadata");
         }
      }
      else if (log.isTraceEnabled())
      {
         log.trace("updateContentMetadata(): content is unchanged");
      }
      this.currentContentMetadata = null;
   }
   
   private void createItemMetadataFromScan(RepositoryRootMetadata rmd, 
                                           RepositoryRootMetadata existingRMD, 
                                           VirtualFile file, VirtualFile root) 
        throws IOException
   {      
      boolean directory = isDirectory(file);
      long timestamp = file.getLastModified();
      
      List<String> pathElements = getRelativePath(file, root);
      RepositoryItemMetadata existing = existingRMD == null ? null : existingRMD.getItemMetadata(pathElements);
      
      // If there's an existing item, assume for now it's unchanged and keep existing originator
      String originator = existing == null ? this.localNodeName : existing.getOriginatingNode();      
      RepositoryItemMetadata md = new RepositoryItemMetadata(pathElements, timestamp, originator, directory, false);
      if (md.equals(existing) == false)
      {
         // above if test failing means this is a new item or
         // timestamp, removed or directory status has changed
         // In any case, this node is now the originator
         md.setOriginatingNode(this.localNodeName);
      }
      
      rmd.getContent().add(md);
      
      if (directory)
      {
         for(VirtualFile child: file.getChildren())
         {
            createItemMetadataFromScan(rmd, existingRMD, child, root);
         }
      }
   }

   private void validateSynchronization(SynchronizationId<?> id)
   {
      if (id == null)
      {
         throw new IllegalArgumentException("Null id");
      }
      
      if (this.currentSynchronizationActionContext == null)
      {
         throw new IllegalStateException("No active synchronization");
      }
      
      SynchronizationId<?> ours = this.currentSynchronizationActionContext.getId();
      if (id.equals(ours) == false)
      {
         throw new IllegalStateException(id + " does not match the current synchronization " + ours);
      }
   }
   
   private static boolean isDirectory(VirtualFile file) throws IOException
   {
      return (!file.isLeaf() && !file.isArchive());
   }
   
   private static List<String> getRelativePath(VirtualFile file, VirtualFile root)
      throws IOException
   {
      List<String> reversed = new ArrayList<String>();
      VirtualFile now = file;
      while(now != null && now.equals(root) == false)
      {
         reversed.add(now.getName());
         now = now.getParent();
      }
      
      if (now == null)
      {
         throw new IllegalArgumentException(file + " is not a child of " + root);
      }
      
      List<String> forward = new ArrayList<String>(reversed.size());
      for (int i = reversed.size() - 1; i > -1; i--)
      {
         forward.add(reversed.get(i));
      }
      
      return forward;
   }

}
