/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.jcr.impl.dataflow.persistent;

import org.exoplatform.services.jcr.dataflow.ChangesLogIterator;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.PersistentDataManager;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLogImpl;
import org.exoplatform.services.jcr.dataflow.ReadOnlyThroughChanges;
import org.exoplatform.services.jcr.dataflow.TransactionChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListenerFilter;
import org.exoplatform.services.jcr.dataflow.persistent.MandatoryItemsPersistenceListener;
import org.exoplatform.services.jcr.dataflow.persistent.PersistedItemData;
import org.exoplatform.services.jcr.dataflow.persistent.PersistedNodeData;
import org.exoplatform.services.jcr.dataflow.persistent.PersistedPropertyData;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.ItemType;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.datamodel.ValueData;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.itemfilters.QPathEntryFilter;
import org.exoplatform.services.jcr.impl.dataflow.TransientValueData;
import org.exoplatform.services.jcr.impl.dataflow.session.TransactionableResourceManager;
import org.exoplatform.services.jcr.impl.dataflow.session.TransactionableResourceManagerListener;
import org.exoplatform.services.jcr.impl.storage.SystemDataContainerHolder;
import org.exoplatform.services.jcr.storage.WorkspaceDataContainer;
import org.exoplatform.services.jcr.storage.WorkspaceStorageConnection;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.InvalidItemStateException;
import javax.jcr.RepositoryException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.TransactionManager;

/**
 * Created by The eXo Platform SAS.<br>
 * Workspace-level data manager. Connects persistence layer with <code>WorkspaceDataContainer</code>
 * instance. Provides read and save operations. Handles listeners on save operation.
 * 
 * @author <a href="mailto:gennady.azarenkov@exoplatform.com">Gennady Azarenkov</a>
 * @version $Id$
 */
public abstract class WorkspacePersistentDataManager implements PersistentDataManager
{

   /**
    * Logger.
    */
   protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.WorkspacePersistentDataManager");

   /**
    * Workspace data container (persistent storage).
    */
   protected final WorkspaceDataContainer dataContainer;

   /**
    * System workspace data container (persistent storage).
    */
   protected final WorkspaceDataContainer systemDataContainer;

   /**
    * Persistent level listeners. This listeners can be filtered by filters from
    * <code>liestenerFilters</code> list.
    */
   protected final List<ItemsPersistenceListener> listeners;

   /**
    * Mandatory persistent level listeners.
    */
   protected final List<MandatoryItemsPersistenceListener> mandatoryListeners;

   /**
    * Persistent level listeners filters.
    */
   protected final List<ItemsPersistenceListenerFilter> listenerFilters;

   /**
    * Read-only status.
    */
   protected boolean readOnly = false;

   /**
    * The resource manager
    */
   private final TransactionableResourceManager txResourceManager;

   /**
    * The transaction manager
    */
   protected final TransactionManager transactionManager;
   /**
    * Changes log wrapper adds possibility to replace changes log.
    * Changes log contains transient data on save but listeners should be notifyed
    * with persisted data only.
    */
   protected class ChangesLogWrapper implements ItemStateChangesLog
   {
      private ItemStateChangesLog log;

      ChangesLogWrapper(ItemStateChangesLog log)
      {
         this.log = log;
      }

      /**
       * Replace log with persisted data only.
       */
      protected void setLog(ItemStateChangesLog log)
      {
         this.log = log;
      }

      protected ItemStateChangesLog getChangesLog()
      {
         return log;
      }

      /**
       * {@inheritDoc}
       */
      public List<ItemState> getAllStates()
      {
         return log.getAllStates();
      }

      /**
       * {@inheritDoc}
       */
      public int getSize()
      {
         return log.getSize();
      }

      /**
       * {@inheritDoc}
       */
      public String dump()
      {
         return log.dump();
      }
   }

   /**
    * WorkspacePersistentDataManager constructor.
    * 
    * @param dataContainer
    *          workspace data container
    * @param systemDataContainerHolder
    *          holder of system workspace data container
    */
   protected WorkspacePersistentDataManager(WorkspaceDataContainer dataContainer,
      SystemDataContainerHolder systemDataContainerHolder)
   {
      this(dataContainer, systemDataContainerHolder, null, null);
   }

   /**
    * WorkspacePersistentDataManager constructor.
    * 
    * @param dataContainer
    *          workspace data container
    * @param systemDataContainerHolder
    *          holder of system workspace data container
    * @param txResourceManager
    *          the resource manager used to manage the whole tx
    */
   public WorkspacePersistentDataManager(WorkspaceDataContainer dataContainer,
      SystemDataContainerHolder systemDataContainerHolder, TransactionableResourceManager txResourceManager, TransactionManager transactionManager)
   {
      this.dataContainer = dataContainer;
      this.systemDataContainer = systemDataContainerHolder.getContainer();

      this.listeners = new ArrayList<ItemsPersistenceListener>();
      this.mandatoryListeners = new ArrayList<MandatoryItemsPersistenceListener>();
      this.listenerFilters = new ArrayList<ItemsPersistenceListenerFilter>();
      this.txResourceManager = txResourceManager;
      this.transactionManager = transactionManager;
   }

   /**
    * {@inheritDoc}
    */
   public void save(ChangesLogWrapper logWrapper) throws RepositoryException
   {
      save(logWrapper, txResourceManager);
   }
   
   /**
    * {@inheritDoc}
    */
   void save(final ChangesLogWrapper logWrapper, TransactionableResourceManager txResourceManager) throws RepositoryException
   {
      final ItemStateChangesLog changesLog = logWrapper.getChangesLog();

      // check if this workspace container is not read-only
      if (readOnly && !(changesLog instanceof ReadOnlyThroughChanges))
      {
         throw new ReadOnlyWorkspaceException("Workspace container '" + dataContainer.getName() + "' is read-only.");
      }

      final ChangesLogPersister persister = new ChangesLogPersister();

      // whole log will be reconstructed with persisted data 
      ItemStateChangesLog persistedLog;
      boolean failed = true;
      ConnectionMode mode = getMode(txResourceManager);
      try
      {
         if (changesLog instanceof PlainChangesLogImpl)
         {
            persistedLog = persister.save((PlainChangesLogImpl)changesLog);
         }
         else if (changesLog instanceof TransactionChangesLog)
         {
            TransactionChangesLog orig = (TransactionChangesLog)changesLog;

            TransactionChangesLog persisted = new TransactionChangesLog();
            persisted.setSystemId(orig.getSystemId());

            for (ChangesLogIterator iter = orig.getLogIterator(); iter.hasNextLog();)
            {
               persisted.addLog(persister.save(iter.nextLog()));
            }

            persistedLog = persisted;
         }
         else
         {
            // we don't support other types now... i.e. add else-if for that type here
            throw new RepositoryException("Unsupported changes log class " + changesLog.getClass());
         }
         // replace log with persisted data only
         logWrapper.setLog(persistedLog);
         persister.prepare();
         notifySaveItems(persistedLog, true);
         onCommit(persister, mode, txResourceManager);
         failed = false;
      }
      catch (IOException e)
      {
         throw new RepositoryException("Save error", e);
      }
      finally
      {
         persister.clear();
         if (failed)
         {
            persister.rollback();
         }
      }
   }

   /**
    * @return the current tx mode
    */
   private ConnectionMode getMode(TransactionableResourceManager txResourceManager)
   {
      if (txResourceManager != null && txResourceManager.isGlobalTxActive())
      {
         return ConnectionMode.GLOBAL_TX;
      }
      else if (transactionManager != null)
      {
         return ConnectionMode.WITH_TRANSACTION_MANAGER;
      }
      return ConnectionMode.NORMAL;
   }

   /**
    * @param persister
    * @throws RepositoryException
    */
   private void onCommit(final ChangesLogPersister persister, ConnectionMode mode, TransactionableResourceManager txResourceManager) throws RepositoryException
   {
      if (mode == ConnectionMode.NORMAL)
      {
         // The commit is done normally
         persister.commit();
      }
      else if (mode == ConnectionMode.WITH_TRANSACTION_MANAGER)
      {
         try
         {
            transactionManager.getTransaction().registerSynchronization(new Synchronization()
            {

               public void beforeCompletion()
               {
               }

               public void afterCompletion(int status)
               {
                  switch (status)
                  {
                     case Status.STATUS_COMMITTED:
                        try
                        {
                           persister.commit();
                        }
                        catch (Exception e)
                        {
                           throw new RuntimeException("Could not commit the transaction", e);
                        }
                        break;
                     case Status.STATUS_UNKNOWN:
                        LOG.warn("Status UNKNOWN received in afterCompletion method, some data could have been corrupted !!");
                     case Status.STATUS_MARKED_ROLLBACK:
                     case Status.STATUS_ROLLEDBACK:
                        try
                        {
                           persister.rollback();
                        }
                        catch (Exception e)
                        {
                           LOG.error("Could not roll back the transaction", e);
                        }
                        break;

                     default:
                        throw new IllegalStateException("illegal status: " + status);
                  }                  
               }
            });
         }
         catch (Exception e)
         {
            throw new RepositoryException("Cannot register the synchronization for a late commit", e);
         }
      }
      else if (mode == ConnectionMode.GLOBAL_TX)
      {
         // The commit or rollback will be done by callback once the tx will be completed since it could
         // fail later in the tx
         txResourceManager.addListener(new TransactionableResourceManagerListener()
         {

            public void onCommit(boolean onePhase) throws Exception
            {
               persister.commit();
            }

            public void onAfterCompletion(int status) throws Exception
            {
            }

            public void onAbort() throws Exception
            {
               persister.rollback();
            }
         });
      }
   }

   private enum ConnectionMode {
      NORMAL, WITH_TRANSACTION_MANAGER, GLOBAL_TX
   }

   class ChangesLogPersister
   {

      private final Set<QPath> addedNodes = new HashSet<QPath>();

      private WorkspaceStorageConnection thisConnection = null;

      private WorkspaceStorageConnection systemConnection = null;

      protected void commit() throws IllegalStateException, RepositoryException
      {
         if (thisConnection != null)
         {
            thisConnection.commit();
         }
         if (systemConnection != null && !systemConnection.equals(thisConnection))
         {
            systemConnection.commit();
         }
      }

      protected void prepare() throws IllegalStateException, RepositoryException
      {
         if (thisConnection != null && thisConnection.isOpened())
         {
            thisConnection.prepare();
         }
         if (systemConnection != null && !systemConnection.equals(thisConnection) && systemConnection.isOpened())
         {
            systemConnection.prepare();
         }
      }

      protected void clear()
      {
         // help to GC
         addedNodes.clear();
      }

      protected void rollback() throws IllegalStateException, RepositoryException
      {
         if (thisConnection != null && thisConnection.isOpened())
         {
            thisConnection.rollback();
         }
         if (systemConnection != null && !systemConnection.equals(thisConnection) && systemConnection.isOpened())
         {
            systemConnection.rollback();
         }
      }

      protected WorkspaceStorageConnection getSystemConnection() throws RepositoryException
      {
         return systemConnection == null
         // we need system connection but it's not exist
            ? systemConnection = (systemDataContainer != dataContainer // NOSONAR
            // if it's different container instances
               ? systemDataContainer.equals(dataContainer) && thisConnection != null
               // but container confugrations are same and non-system connnection open
                  // reuse this connection as system
                  ? systemDataContainer.reuseConnection(thisConnection)
                  // or open one new system
                  : systemDataContainer.openConnection(false)
               // else if it's same container instances (system and this)
               : thisConnection == null
               // and non-system connection doens't exist - open it
                  ? thisConnection = dataContainer.openConnection(false)
                  // if already open - use it
                  : thisConnection)
            // system connection opened - use it
            : systemConnection;
      }

      protected WorkspaceStorageConnection getThisConnection() throws RepositoryException
      {
         return thisConnection == null
         // we need this conatiner conection
            ? thisConnection = (systemDataContainer != dataContainer // NOSONAR
            // if it's different container instances
               ? dataContainer.equals(systemDataContainer) && systemConnection != null
               // but container confugrations are same and system connnection open
                  // reuse system connection as this
                  ? dataContainer.reuseConnection(systemConnection)
                  // or open one new
                  : dataContainer.openConnection(false)
               // else if it's same container instances (system and this)
               : systemConnection == null
               // and system connection doens't exist - open it
                  ? systemConnection = dataContainer.openConnection(false)
                  // if already open - use it
                  : systemConnection)
            // this connection opened - use it
            : thisConnection;
      }

      protected PlainChangesLogImpl save(PlainChangesLog changesLog) throws InvalidItemStateException,
         RepositoryException, IOException
      {
         // copy state
         PlainChangesLogImpl newLog = PlainChangesLogImpl.createCopy(new ArrayList<ItemState>(), changesLog);

         Map<String, PropertyData> lastUpdateStates = new HashMap<String, PropertyData>();

         for (Iterator<ItemState> iter = changesLog.getAllStates().iterator(); iter.hasNext();)
         {
            ItemState prevState = iter.next();
            ItemData newData;

            if (prevState.getData() instanceof PersistedItemData)
            {
               // use existing if persisted 
               newData = prevState.getData();
            }
            else
            {
               // copy transient as persisted 
               if (prevState.isNode())
               {
                  NodeData prevData = (NodeData)prevState.getData();
                  newData =
                     new PersistedNodeData(prevData.getIdentifier(), prevData.getQPath(), prevData
                        .getParentIdentifier(), prevData.getPersistedVersion() + 1, prevData.getOrderNumber(), prevData
                        .getPrimaryTypeName(), prevData.getMixinTypeNames(), prevData.getACL());
               }
               else
               {
                  PropertyData prevData = (PropertyData)prevState.getData();

                  if (prevData.getValues() != null) // null if it's DELETE state
                  {
                     List<ValueData> values = new ArrayList<ValueData>();

                     if (prevState.isRenamed() && lastUpdateStates.containsKey(prevState.getData().getIdentifier()))
                     {
                        values = lastUpdateStates.get(prevState.getData().getIdentifier()).getValues();
                     }
                     else
                     {
                        for (int i = 0; i < prevData.getValues().size(); i++)
                        {
                           ValueData vd = prevData.getValues().get(i);

                           if (vd instanceof TransientValueData)
                           {
                              TransientValueData tvd = (TransientValueData)vd;
                              ValueData pvd;

                              if (vd.isByteArray())
                              {
                                 pvd = new ByteArrayPersistedValueData(i, vd.getAsByteArray());
                                 values.add(pvd);
                              }
                              else
                              {
                                 File destFile = null;

                                 if (tvd.getSpoolFile() != null)
                                 {
                                    // spooled to temp file
                                    pvd = new StreamPersistedValueData(i, tvd.getSpoolFile(), destFile);
                                 }
                                 else
                                 {
                                    // with original stream
                                    pvd = new StreamPersistedValueData(i, tvd.getOriginalStream(), destFile);
                                 }

                                 values.add(pvd);
                              }

                              tvd.delegate(pvd);
                           }
                           else
                           {
                              values.add(vd);
                           }
                        }
                     }

                     newData =
                        new PersistedPropertyData(prevData.getIdentifier(), prevData.getQPath(), prevData
                           .getParentIdentifier(), prevData.getPersistedVersion() + 1, prevData.getType(), prevData
                           .isMultiValued(), values);

                     if (prevState.isAdded() || prevState.isUpdated())
                     {
                        lastUpdateStates.put(prevState.getData().getIdentifier(), (PropertyData)newData);
                     }
                  }
                  else
                  {
                     newData =
                        new PersistedPropertyData(prevData.getIdentifier(), prevData.getQPath(), prevData
                           .getParentIdentifier(), prevData.getPersistedVersion() + 1, prevData.getType(), prevData
                           .isMultiValued(), null);
                  }
               }
            }

            ItemState itemState =
               new ItemState(newData, prevState.getState(), prevState.isEventFire(), prevState.getAncestorToSave(),
                  prevState.isInternallyCreated(), prevState.isPersisted(), prevState.getOldPath());

            newLog.add(itemState);

            // save state
            if (itemState.isPersisted())
            {
               long start = 0;
               if (LOG.isDebugEnabled())
               {
                  start = System.currentTimeMillis();
               }
               ItemData data = itemState.getData();

               WorkspaceStorageConnection conn;
               if (isSystemDescendant(data.getQPath()))
               {
                  conn = getSystemConnection();
               }
               else
               {
                  conn = getThisConnection();
               }

               if (itemState.isAdded())
               {
                  doAdd(data, conn, addedNodes);
               }
               else if (itemState.isUpdated())
               {
                  doUpdate(data, conn);
               }
               else if (itemState.isDeleted())
               {
                  doDelete(data, conn);
               }
               else if (itemState.isRenamed())
               {
                  doRename(data, conn, addedNodes);
               }

               if (LOG.isDebugEnabled())
               {
                  LOG.debug(ItemState.nameFromValue(itemState.getState()) + " " + (System.currentTimeMillis() - start)
                     + "ms, " + data.getQPath().getAsString());
               }
            }
         }

         lastUpdateStates.clear(); //help to GC

         return newLog;
      }
   }

   /**
    * {@inheritDoc}
    */
   public ItemData getItemData(final String identifier) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getItemData(identifier);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> getReferencesData(final String identifier, boolean skipVersionStorage)
      throws RepositoryException
   {

      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         final List<PropertyData> allRefs = con.getReferencesData(identifier);
         final List<PropertyData> refProps = new ArrayList<PropertyData>();
         for (int i = 0; i < allRefs.size(); i++)
         {
            PropertyData ref = allRefs.get(i);
            if (skipVersionStorage)
            {
               if (!ref.getQPath().isDescendantOf(Constants.JCR_VERSION_STORAGE_PATH))
               {
                  refProps.add(ref);
               }
            }
            else
            {
               refProps.add(ref);
            }
         }
         return refProps;
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<NodeData> getChildNodesData(final NodeData nodeData) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getChildNodesData(nodeData);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public boolean getChildNodesDataByPage(final NodeData nodeData, int fromOrderNum, int toOrderNum,
      List<NodeData> childNodes) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getChildNodesDataByPage(nodeData, fromOrderNum, toOrderNum, childNodes);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<NodeData> getChildNodesData(final NodeData nodeData, List<QPathEntryFilter> patternFilters)
      throws RepositoryException
   {

      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getChildNodesData(nodeData, patternFilters);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public int getLastOrderNumber(final NodeData nodeData) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getLastOrderNumber(nodeData);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public int getChildNodesCount(NodeData parent) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getChildNodesCount(parent);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> getChildPropertiesData(final NodeData nodeData) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getChildPropertiesData(nodeData);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> getChildPropertiesData(final NodeData nodeData,
      final List<QPathEntryFilter> itemDataFilters) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getChildPropertiesData(nodeData, itemDataFilters);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> listChildPropertiesData(final NodeData nodeData) throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.listChildPropertiesData(nodeData);
      }
      finally
      {
         con.close();
      }
   }

   // ----------------------------------------------
   /**
    * Check if given node path contains index higher 1 and if yes if same-name sibling exists in
    * persistence or in current changes log.
    */
   private void checkSameNameSibling(NodeData node, WorkspaceStorageConnection con, final Set<QPath> addedNodes)
      throws RepositoryException
   {
      if (node.getQPath().getIndex() > 1)
      {
         // check if an older same-name sibling exists
         // the check is actual for all operations including delete

         final QPathEntry[] path = node.getQPath().getEntries();
         final QPathEntry[] siblingPath = new QPathEntry[path.length];
         final int li = path.length - 1;
         System.arraycopy(path, 0, siblingPath, 0, li);

         siblingPath[li] = new QPathEntry(path[li], path[li].getIndex() - 1);

         if (addedNodes.contains(new QPath(siblingPath)))
         {
            // current changes log has the older same-name sibling
            return;
         }
         else
         {
            // check in persistence
            if (dataContainer.isCheckSNSNewConnection())
            {
               final WorkspaceStorageConnection acon = dataContainer.openConnection();
               try
               {
                  checkPersistedSNS(node, acon);
               }
               finally
               {
                  acon.close();
               }
            }
            else
            {
               checkPersistedSNS(node, con);
            }
         }
      }
   }

   /**
    * Check if same-name sibling exists in persistence.
    */
   private void checkPersistedSNS(NodeData node, WorkspaceStorageConnection acon) throws RepositoryException
   {
      NodeData parent = (NodeData)acon.getItemData(node.getParentIdentifier());
      QPathEntry myName = node.getQPath().getEntries()[node.getQPath().getEntries().length - 1];
      ItemData sibling =
         acon.getItemData(parent, new QPathEntry(myName.getNamespace(), myName.getName(), myName.getIndex() - 1),
            ItemType.NODE);

      if (sibling == null || !sibling.isNode())
      {
         throw new InvalidItemStateException("Node can't be saved " + node.getQPath().getAsString()
            + ". No same-name sibling exists with index " + (myName.getIndex() - 1) + ".");
      }
   }

   /**
    * Performs actual item data deleting.
    * 
    * @param item
    *          to delete
    * @param con
    * @throws RepositoryException
    * @throws InvalidItemStateException
    *           if the item is already deleted
    */
   protected void doDelete(final ItemData item, final WorkspaceStorageConnection con) throws RepositoryException,
      InvalidItemStateException
   {

      if (item.isNode())
      {
         con.delete((NodeData)item);
      }
      else
      {
         con.delete((PropertyData)item);
      }
   }

   /**
    * Performs actual item data updating.
    * 
    * @param item
    *          to update
    * @param con
    *          connection
    * @throws RepositoryException
    * @throws InvalidItemStateException
    *           if the item not found
    */
   protected void doUpdate(final ItemData item, final WorkspaceStorageConnection con) throws RepositoryException,
      InvalidItemStateException
   {

      if (item.isNode())
      {
         con.update((NodeData)item);
      }
      else
      {
         con.update((PropertyData)item);
      }
   }

   /**
    * Performs actual item data adding.
    * 
    * @param item
    *          to add
    * @param con
    *          connection
    * @throws RepositoryException
    * @throws InvalidItemStateException
    *           if the item is already added
    */
   protected void doAdd(final ItemData item, final WorkspaceStorageConnection con, final Set<QPath> addedNodes)
      throws RepositoryException, InvalidItemStateException
   {

      if (item.isNode())
      {
         final NodeData node = (NodeData)item;

         checkSameNameSibling(node, con, addedNodes);
         addedNodes.add(node.getQPath());

         con.add(node);
      }
      else
      {
         con.add((PropertyData)item);
      }
   }

   /**
    * Perform node rename.
    * 
    * @param item
    * @param con
    * @param addedNodes
    * @throws RepositoryException
    * @throws InvalidItemStateException
    */
   protected void doRename(final ItemData item, final WorkspaceStorageConnection con, final Set<QPath> addedNodes)
      throws RepositoryException, InvalidItemStateException
   {
      final NodeData node = (NodeData)item;

      checkSameNameSibling(node, con, addedNodes);
      addedNodes.add(node.getQPath());

      con.rename(node);
   }

   /**
    * 
    * Get current container time.
    * 
    * @return current time
    */
   public Calendar getCurrentTime()
   {
      return dataContainer.getCurrentTime();
   }

   // ---------------------------------------------

   /**
    * Adds listener to the list.
    * 
    * @param listener
    */
   public void addItemPersistenceListener(ItemsPersistenceListener listener)
   {
      if (listener instanceof MandatoryItemsPersistenceListener)
      {
         mandatoryListeners.add((MandatoryItemsPersistenceListener)listener);
      }
      else
      {
         listeners.add(listener);
      }
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Workspace '" + this.dataContainer.getName() + "' listener registered: " + listener);
      }
   }

   public void removeItemPersistenceListener(ItemsPersistenceListener listener)
   {
      if (listener instanceof MandatoryItemsPersistenceListener)
      {
         mandatoryListeners.remove(listener);
      }
      else
      {
         listeners.remove(listener);
      }

      if (LOG.isDebugEnabled())
      {
         LOG.debug("Workspace '" + this.dataContainer.getName() + "' listener unregistered: " + listener);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void addItemPersistenceListenerFilter(ItemsPersistenceListenerFilter filter)
   {
      this.listenerFilters.add(filter);
   }

   /**
    * {@inheritDoc}
    */
   public void removeItemPersistenceListenerFilter(ItemsPersistenceListenerFilter filter)
   {
      this.listenerFilters.remove(filter);
   }

   /**
    * Check if the listener can be accepted. If at least one filter doesn't accept the listener it
    * returns false, true otherwise.
    * 
    * @param listener
    *          ItemsPersistenceListener
    * @return boolean, true if accepted, false otherwise.
    */
   protected boolean isListenerAccepted(ItemsPersistenceListener listener)
   {
      for (ItemsPersistenceListenerFilter f : listenerFilters)
      {
         if (!f.accept(listener))
         {
            return false;
         }
      }

      return true;
   }

   /**
    * Notify listeners about current changes log persistent state.
    * Listeners notified according to is listener transaction aware.
    * 
    * @param changesLog
    *          ItemStateChangesLog
    * @param isListenerTXAware - is listeners notified in transaction, or after transaction
    */
   protected void notifySaveItems(final ItemStateChangesLog changesLog, boolean isListenerTXAware)
   {
      for (MandatoryItemsPersistenceListener mlistener : mandatoryListeners)
      {
         if (mlistener.isTXAware() == isListenerTXAware)
         {
            mlistener.onSaveItems(changesLog);
         }
      }

      for (ItemsPersistenceListener listener : listeners)
      {
         if (listener.isTXAware() == isListenerTXAware && isListenerAccepted(listener))
         {
            listener.onSaveItems(changesLog);
         }
      }
   }

   /**
    * Tell if the path is jcr:system descendant.
    * 
    * @param path
    *          path to check
    * @return boolean result, true if yes - it's jcr:system tree path
    */
   private boolean isSystemDescendant(QPath path)
   {
      return path.isDescendantOf(Constants.JCR_SYSTEM_PATH) || path.equals(Constants.JCR_SYSTEM_PATH);
   }

   /**
    * {@inheritDoc}
    */
   public ItemData getItemData(final NodeData parentData, final QPathEntry name) throws RepositoryException
   {
      return getItemData(parentData, name, ItemType.UNKNOWN);
   }

   /**
    * {@inheritDoc}
    */
   public ItemData getItemData(final NodeData parentData, final QPathEntry name, ItemType itemType)
      throws RepositoryException
   {
      final WorkspaceStorageConnection con = dataContainer.openConnection();
      try
      {
         return con.getItemData(parentData, name, itemType);
      }
      finally
      {
         con.close();
      }
   }

   /**
    * {@inheritDoc}true
    */
   public boolean isReadOnly()
   {
      return readOnly;
   }

   /**
    * {@inheritDoc}
    */
   public void setReadOnly(boolean status)
   {
      this.readOnly = status;
   }
}
