/* * 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.core; import org.exoplatform.services.jcr.access.AccessControlList; import org.exoplatform.services.jcr.access.AccessManager; import org.exoplatform.services.jcr.access.PermissionType; import org.exoplatform.services.jcr.core.nodetype.ItemDefinitionData; import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions; import org.exoplatform.services.jcr.dataflow.DataManager; import org.exoplatform.services.jcr.dataflow.ItemDataConsumer; import org.exoplatform.services.jcr.dataflow.ItemState; import org.exoplatform.services.jcr.dataflow.PlainChangesLog; import org.exoplatform.services.jcr.dataflow.SharedDataManager; import org.exoplatform.services.jcr.dataflow.persistent.PersistedPropertyData; import org.exoplatform.services.jcr.datamodel.IllegalPathException; 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.impl.Constants; import org.exoplatform.services.jcr.impl.core.itemfilters.QPathEntryFilter; import org.exoplatform.services.jcr.impl.core.version.ChildVersionRemoveVisitor; import org.exoplatform.services.jcr.impl.core.version.VersionHistoryImpl; import org.exoplatform.services.jcr.impl.core.version.VersionImpl; import org.exoplatform.services.jcr.impl.dataflow.ItemDataMoveVisitor; import org.exoplatform.services.jcr.impl.dataflow.TransientNodeData; import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData; import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil; import org.exoplatform.services.jcr.impl.dataflow.persistent.LocalWorkspaceDataManagerStub; import org.exoplatform.services.jcr.impl.dataflow.persistent.SimplePersistedSize; import org.exoplatform.services.jcr.impl.dataflow.session.SessionChangesLog; import org.exoplatform.services.jcr.impl.dataflow.session.TransactionableDataManager; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.jboss.util.collection.WeakValueHashMap; import javax.jcr.AccessDeniedException; import javax.jcr.InvalidItemStateException; import javax.jcr.ItemExistsException; import javax.jcr.ReferentialIntegrityException; import javax.jcr.RepositoryException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.VersionException; /** * Created by The eXo Platform SAS.<br> * * @author <a href="mailto:geaz@users.sourceforge.net">Gennady Azarenkov </a> * @version $Id: SessionDataManager.java 14590 2008-05-22 08:51:29Z pnedonosko $ */ public class SessionDataManager implements ItemDataConsumer { public static final int MERGE_NODES = 1; public static final int MERGE_PROPS = 2; public static final int MERGE_ITEMS = 3; protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.SessionDataManager"); protected final SessionImpl session; protected final ItemReferencePool itemsPool; /** * Contains items was deleted but still not saved. i.e. deleted in session. The list will be * cleared by each session save call. */ protected final List<ItemImpl> invalidated = new ArrayList<ItemImpl>(); private final SessionChangesLog changesLog; protected final SessionItemFactory itemFactory; protected final AccessManager accessManager; protected final TransactionableDataManager transactionableManager; public SessionDataManager(SessionImpl session, LocalWorkspaceDataManagerStub dataManager) throws RepositoryException { this.session = session; this.changesLog = new SessionChangesLog(session); this.itemsPool = new ItemReferencePool(); this.itemFactory = new SessionItemFactory(); this.accessManager = session.getAccessManager(); this.transactionableManager = new TransactionableDataManager(dataManager, session); } /** * @return Returns the workspDataManager. */ public SharedDataManager getWorkspaceDataManager() { return transactionableManager.getStorageDataManager(); } public String dump() { StringBuilder d = new StringBuilder("\nChanges:"); d.append(changesLog.dump()).append("\nCache:").append(itemsPool.dump()); return d.toString(); } /** * @return Returns the TransactionableDataManager */ public TransactionableDataManager getTransactManager() { return transactionableManager; } /** * Return item data by internal <b>qpath</b> in this transient storage then in workspace * container. * * @param path * - absolute path * @return existed item data or null if not found * @throws RepositoryException * @see org.exoplatform.services.jcr.dataflow.ItemDataConsumer#getItemData(String) */ public ItemData getItemData(QPath path) throws RepositoryException { NodeData parent = (NodeData)getItemData(Constants.ROOT_UUID); if (path.equals(Constants.ROOT_PATH)) { return parent; } QPathEntry[] relPathEntries = path.getRelPath(path.getDepth()); return getItemData(parent, relPathEntries, ItemType.UNKNOWN); } /** * Return item data by parent NodeDada and relPathEntries If relpath is JCRPath.THIS_RELPATH = '.' * it return itself * * @param parent * @param relPathEntries * - array of QPathEntry which represents the relation path to the searched item * @param itemType * - item type * @return existed item data or null if not found * @throws RepositoryException */ public ItemData getItemData(NodeData parent, QPathEntry[] relPathEntries, ItemType itemType) throws RepositoryException { ItemData item = parent; for (int i = 0; i < relPathEntries.length; i++) { if (i == relPathEntries.length - 1) { item = getItemData(parent, relPathEntries[i], itemType); } else { item = getItemData(parent, relPathEntries[i], ItemType.UNKNOWN); } if (item == null) { break; } if (item.isNode()) { parent = (NodeData)item; } else if (i < relPathEntries.length - 1) { throw new IllegalPathException("Path can not contains a property as the intermediate element"); } } return item; } /** * {@inheritDoc} */ public ItemData getItemData(NodeData parent, QPathEntry name, ItemType itemType) throws RepositoryException { return getItemData(parent, name, false, itemType, true); } /** * {@inheritDoc} */ public ItemData getItemData(NodeData parent, QPathEntry name, ItemType itemType, boolean createNullItemData) throws RepositoryException { return getItemData(parent, name, false, itemType, createNullItemData); } private ItemData getItemData(NodeData parent, QPathEntry name, boolean skipCheckInPersistence, ItemType itemType, boolean createNullItemData) throws RepositoryException { if (name.getName().equals(JCRPath.PARENT_RELPATH) && name.getNamespace().equals(Constants.NS_DEFAULT_URI)) { if (parent.getIdentifier().equals(Constants.ROOT_UUID)) { return null; } else { return getItemData(parent.getParentIdentifier()); } } ItemData data = null; // 1. Try in transient changes ItemState state = changesLog.getItemState(parent, name, itemType); if (state == null) { // 2. Check if the parent node is a new node if (isNew(parent.getIdentifier())) { // The parent node is a new node so we know that the data doesn't exist in the database return null; } // 2. Try from txdatamanager if (!(skipCheckInPersistence)) { data = transactionableManager.getItemData(parent, name, itemType, createNullItemData); data = updatePathIfNeeded(data); } } else if (!state.isDeleted()) { data = state.getData(); } return data; } /** * {@inheritDoc} */ public boolean hasItemData(NodeData parent, QPathEntry name, ItemType itemType) throws RepositoryException { if (name.getName().equals(JCRPath.PARENT_RELPATH) && name.getNamespace().equals(Constants.NS_DEFAULT_URI)) { if (parent.getIdentifier().equals(Constants.ROOT_UUID)) { return false; } else { return true; } } // 1. Try in transient changes ItemState state = changesLog.getItemState(parent, name, itemType); if (state == null) { // 2. Check if the parent node is a new node if (isNew(parent.getIdentifier())) { // The parent node is a new node so we know that the data doesn't exist in the database return false; } return transactionableManager.hasItemData(parent, name, itemType); } else { return !state.isDeleted(); } } /** * Return item data by identifier in this transient storage then in workspace container. * * @param identifier * @param checkChangesLogOnly * @return existed item data or null if not found * @throws RepositoryException * @see org.exoplatform.services.jcr.dataflow.ItemDataConsumer#getItemData(java.lang.String) */ public ItemData getItemData(String identifier,boolean checkChangesLogOnly) throws RepositoryException { ItemData data = null; // 1. Try in transient changes ItemState state = changesLog.getItemState(identifier); if (state == null) { // 2. Try from txdatamanager data = transactionableManager.getItemData(identifier,checkChangesLogOnly); data = updatePathIfNeeded(data); } else if (!state.isDeleted()) { data = state.getData(); } return data; } /** * Return item data by identifier in this transient storage then in workspace container. * * @param identifier * @return existed item data or null if not found * @throws RepositoryException * @see org.exoplatform.services.jcr.dataflow.ItemDataConsumer#getItemData(java.lang.String) */ public ItemData getItemData(String identifier) throws RepositoryException { return getItemData(identifier,false); } /** * Return Item by parent NodeDada and the name of searched item. * * @param parent * - parent of the searched item * @param name * - item name * @param itemType * - item type * @param pool * - indicates does the item fall in pool * @return existed item or null if not found * @throws RepositoryException */ public ItemImpl getItem(NodeData parent, QPathEntry name, boolean pool, ItemType itemType) throws RepositoryException { return getItem(parent, name, pool, itemType, true); } /** * For internal use, required privileges. Return Item by parent NodeDada and the name of searched item. * * @param parent * - parent of the searched item * @param name * - item name * @param itemType * - item type * @param pool * - indicates does the item fall in pool * @param apiRead * - if true will call postRead Action and check permissions * @return existed item or null if not found * @throws RepositoryException */ public ItemImpl getItem(NodeData parent, QPathEntry name, boolean pool, ItemType itemType, boolean apiRead) throws RepositoryException { return getItem(parent, name, pool, itemType, apiRead, true); } /** * For internal use. Return Item by parent NodeDada and the name of searched item. * * @param parent * - parent of the searched item * @param name * - item name * @param itemType * - item type * @param pool * - indicates does the item fall in pool * @param apiRead * - if true will call postRead Action and check permissions * @param createNullItemData * - defines if there is a need to create NullItemData * * @return existed item or null if not found * @throws RepositoryException */ public ItemImpl getItem(NodeData parent, QPathEntry name, boolean pool, ItemType itemType, boolean apiRead, boolean createNullItemData) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getItem(" + parent.getQPath().getAsString() + " + " + name.getAsString() + " ) >>>>>"); } ItemImpl item = null; try { return item = readItem(getItemData(parent, name, itemType, createNullItemData), parent, pool, apiRead); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getItem(" + parent.getQPath().getAsString() + " + " + name.getAsString() + ") --> " + (item != null ? item.getPath() : "null") + " <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } public ItemImpl getItem(NodeData parent, QPathEntry name, boolean pool, boolean skipCheckInPersistence, ItemType itemType) throws RepositoryException { return getItem(parent, name, pool, skipCheckInPersistence, itemType, true); } /** * Return Item by parent NodeDada and the name of searched item. * * @param parent * - parent of the searched item * @param name * - item name * @param pool * - indicates does the item fall in pool * @param skipCheckInPersistence * - skip getting Item from persistence if need * @param itemType * - item type * @return existed item or null if not found * @throws RepositoryException */ public ItemImpl getItem(NodeData parent, QPathEntry name, boolean pool, boolean skipCheckInPersistence, ItemType itemType, boolean createNullItemData) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getItem(" + parent.getQPath().getAsString() + " + " + name.getAsString() + " ) >>>>>"); } ItemImpl item = null; try { return item = readItem(getItemData(parent, name, skipCheckInPersistence, itemType, createNullItemData), parent, pool, true); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getItem(" + parent.getQPath().getAsString() + " + " + name.getAsString() + ") --> " + (item != null ? item.getPath() : "null") + " <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * Return Item by parent NodeDada and array of QPathEntry which represent a relative path to the * searched item * * @param parent * - parent of the searched item * @param relPath * - array of QPathEntry which represents the relation path to the searched item * @param pool * - indicates does the item fall in pool * @param itemType * - item type * @return existed item or null if not found * @throws RepositoryException */ public ItemImpl getItem(NodeData parent, QPathEntry[] relPath, boolean pool, ItemType itemType) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); StringBuilder debugPath = new StringBuilder(); for (QPathEntry rp : relPath) { debugPath.append(rp.getAsString()); } LOG.debug("getItem(" + parent.getQPath().getAsString() + " + " + debugPath + " ) >>>>>"); } ItemImpl item = null; try { return item = readItem(getItemData(parent, relPath, itemType), pool); } finally { if (LOG.isDebugEnabled()) { StringBuilder debugPath = new StringBuilder(); for (QPathEntry rp : relPath) { debugPath.append(rp.getAsString()); } LOG.debug("getItem(" + parent.getQPath().getAsString() + " + " + debugPath + ") --> " + (item != null ? item.getPath() : "null") + " <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * Return item by absolute path in this transient storage then in workspace container. * * @param path * - absolute path to the searched item * @param pool * - indicates does the item fall in pool * @return existed item or null if not found * @throws RepositoryException */ public ItemImpl getItem(QPath path, boolean pool) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getItem(" + path.getAsString() + " ) >>>>>"); } ItemImpl item = null; try { return item = readItem(getItemData(path), pool); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getItem(" + path.getAsString() + ") --> " + (item != null ? item.getPath() : "null") + " <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * Read ItemImpl of given ItemData. * Will call postRead Action and check permissions. * * @param itemData ItemData * @param pool boolean, if true will reload pooled ItemImpl * @return ItemImpl * @throws RepositoryException if errro occurs */ protected ItemImpl readItem(ItemData itemData, boolean pool) throws RepositoryException { return readItem(itemData, null, pool, true); } /** * Create or reload pooled ItemImpl with the given ItemData. * * @param itemData ItemData, data to create ItemImpl * @param parent NodeData, this item parent data, can be null. Not null used for getChildXXX() * @param pool boolean, if true will reload pooled ItemImpl * @param apiRead boolean, if true will call postRead Action and check permissions * @return ItemImpl * @throws RepositoryException if error occurs */ protected ItemImpl readItem(ItemData itemData, NodeData parent, boolean pool, boolean apiRead) throws RepositoryException { if (!apiRead) { // Need privileges SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkPermission(JCRRuntimePermissions.INVOKE_INTERNAL_API_PERMISSION); } } if (itemData != null) { ItemImpl item; ItemImpl pooledItem; if (pool && (pooledItem = itemsPool.get(itemData, parent)) != null) { // use pooled & reloaded item = pooledItem; } else { // create new item = itemFactory.createItem(itemData, parent); } if (apiRead) { if (!item.hasPermission(PermissionType.READ)) { throw new AccessDeniedException("Access denied " + itemData.getQPath().getAsString() + " for " + session.getUserID()); } session.getActionHandler().postRead(item); } return item; } else { return null; } } /** * Return item by identifier in this transient storage then in workspace container. * * @param identifier * - identifier of searched item * @param pool * - indicates does the item fall in pool * @return existed item data or null if not found * @throws RepositoryException */ public ItemImpl getItemByIdentifier(String identifier, boolean pool) throws RepositoryException { return getItemByIdentifier(identifier, pool, true); } /** * For internal use, required privileges. Return item by identifier in this transient storage then in workspace container. * * @param identifier * - identifier of searched item * @param pool * - indicates does the item fall in pool * @param apiRead * - if true will call postRead Action and check permissions * @return existed item data or null if not found * @throws RepositoryException */ public ItemImpl getItemByIdentifier(String identifier, boolean pool, boolean apiRead) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getItemByIdentifier(" + identifier + " ) >>>>>"); } ItemImpl item = null; try { return item = readItem(getItemData(identifier), null, pool, apiRead); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getItemByIdentifier(" + identifier + ") --> " + (item != null ? item.getPath() : "null") + " <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * Returns true if this Session holds pending (that is, unsaved) changes; otherwise returns false. * * @param path * to the node item * @return boolean */ public boolean hasPendingChanges(QPath path) { return changesLog.getDescendantsChanges(path).size() > 0; } /** * Returns true if the item with <code>identifier</code> is a new item, meaning that it exists * only in transient storage on the Session and has not yet been saved. Within a transaction, * isNew on an Item may return false (because the item has been saved) even if that Item is not in * persistent storage (because the transaction has not yet been committed). * * @param identifier * of the item * @return boolean */ public boolean isNew(String identifier) { ItemState lastState = changesLog.getItemState(identifier); if (lastState == null || lastState.isDeleted()) { return false; } return changesLog.getItemState(identifier, ItemState.ADDED) != null; } /** * Returns true if the item with <code>identifier</code> was deleted in this session. Within a transaction, * isDelete on an Item may return false (because the item has been saved) even if that Item is not in * persistent storage (because the transaction has not yet been committed). * * @param identifier * of the item * @return boolean, true if the item was deleted */ public boolean isDeleted(String identifier) { ItemState lastState = changesLog.getItemState(identifier); if (lastState != null && lastState.isDeleted()) { return true; } return false; } /** * Returns true if the item with <code>itemPath</code> was deleted in this session. Within a transaction, * isDelete on an Item may return false (because the item has been saved) even if that Item is not in * persistent storage (because the transaction has not yet been committed). * * @param itemPath QPath, path of the item * @return boolean, true if the item was deleted */ public boolean isDeleted(QPath itemPath) { ItemState lastState = changesLog.getItemState(itemPath); if (lastState != null && lastState.isDeleted()) { return true; } return false; } /** * Returns true if this Item has been saved but has subsequently been modified through the current * session and therefore the state of this item as recorded in the session differs from the state * of this item as saved. Within a transaction, isModified on an Item may return false (because * the Item has been saved since the modification) even if the modification in question is not in * persistent storage (because the transaction has not yet been committed). * * @param item * ItemData * @return boolean */ public boolean isModified(ItemData item) { if (item.isNode()) { // this node and child changes only Collection<ItemState> nodeChanges = changesLog.getLastModifyStates((NodeData)item); return nodeChanges.size() > 0; } ItemState state = changesLog.getLastState(item, false); if (state != null) { if (state.isAdded() || state.isDeleted()) { return false; } return true; } return false; } /** * Returns saved only references (allowed by specs). * * @param identifier * String * @return List of PropertyImpl * @throws RepositoryException * if error * * @see javax.jcr.Node#getReferences */ public List<PropertyImpl> getReferences(String identifier) throws RepositoryException { List<PropertyData> refDatas = transactionableManager.getReferencesData(identifier, true); List<PropertyImpl> refs = new ArrayList<PropertyImpl>(refDatas.size()); for (int i = 0, length = refDatas.size(); i < length; i++) { PropertyData data = refDatas.get(i); ItemState state = changesLog.getItemState(data.getIdentifier()); if (state != null) { if (state.isDeleted()) { // if the Property was deleted skip it for now continue; } // otherwise use transient data data = (PropertyData)state.getData(); } NodeData parent = (NodeData)getItemData(data.getParentIdentifier()); // if parent exists check for read permissions, otherwise the parent was deleted in another session. if (parent != null) { // skip not permitted if (accessManager.hasPermission(parent.getACL(), new String[]{PermissionType.READ}, session.getUserState() .getIdentity())) { PropertyImpl item = (PropertyImpl)readItem(data, parent, true, false); refs.add(item); session.getActionHandler().postRead(item); } } } return refs; } /** * {@inheritDoc} */ public boolean getChildNodesDataByPage(final NodeData parent, int fromOrderNum,int offset, int pageSize, List<NodeData> childs) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getChildNodesData(" + parent.getQPath().getAsString() + " , itemDataFilter) >>>>>"); } try { boolean hasNext = false; if (!isNew(parent.getIdentifier())) { hasNext = transactionableManager.getChildNodesDataByPage(parent, fromOrderNum, offset, pageSize, childs); } // merge data Collection<ItemState> transientDescendants = changesLog.getLastChildrenStates(parent, true); if (!transientDescendants.isEmpty()) { // 2 get ALL persisted descendants Map<String, NodeData> descendants = new LinkedHashMap<String, NodeData>(); for (int i = 0, length = childs.size(); i < length; i++) { NodeData childNode = childs.get(i); descendants.put(childNode.getIdentifier(), childNode); } int minOrderNum = childs.size() != 0 ? childs.get(0).getOrderNumber() : -1; int maxOrderNum = childs.size() != 0 ? childs.get(childs.size() - 1).getOrderNumber() : -1; // merge data for (ItemState state : transientDescendants) { NodeData data = (NodeData)state.getData(); // we have only last states, so remove nodes first descendants.remove(data.getIdentifier()); if ((state.isAdded() || state.isRenamed() || state.isPathChanged()) && !hasNext) { descendants.put(data.getIdentifier(), data); } else if (state.isMixinChanged() || state.isUpdated()) { if (minOrderNum <= data.getOrderNumber() && data.getOrderNumber() <= maxOrderNum) { descendants.put(data.getIdentifier(), data); } } } childs.clear(); childs.addAll(descendants.values()); } return hasNext; } finally { if (LOG.isDebugEnabled()) { LOG.debug("getChildNodesData(" + parent.getQPath().getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * {@inheritDoc} */ public List<NodeData> getChildNodesData(NodeData parent) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getChildNodesData(" + parent.getQPath().getAsString() + ") >>>>>"); } try { return (List<NodeData>)mergeNodes(parent, transactionableManager); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getChildNodesData(" + parent.getQPath().getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * {@inheritDoc} */ public List<NodeData> getChildNodesData(NodeData parent, List<QPathEntryFilter> patternFilters) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getChildNodesData(" + parent.getQPath().getAsString() + " , itemDataFilter) >>>>>"); } try { List<NodeData> persistChildNodes = (isNew(parent.getIdentifier())) ? new ArrayList<NodeData>() : transactionableManager.getChildNodesData( parent, patternFilters); return (List<NodeData>)mergeNodes(parent, persistChildNodes); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getChildNodesData(" + parent.getQPath().getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * {@inheritDoc} */ public int getLastOrderNumber(NodeData parent) throws RepositoryException { int lastOrderNumber = changesLog.getLastChildOrderNumber(parent.getIdentifier()); int lastPersistedNodeOrderNumber = isNew(parent.getIdentifier()) ? -1 : transactionableManager.getLastOrderNumber(parent); return Math.max(lastPersistedNodeOrderNumber, lastOrderNumber); } /** * {@inheritDoc} */ public int getChildNodesCount(NodeData parent) throws RepositoryException { int childsCount = changesLog.getChildNodesCount(parent.getIdentifier()) + (isNew(parent.getIdentifier()) ? 0 : transactionableManager.getChildNodesCount(parent)); if (childsCount < 0) { throw new InvalidItemStateException("Node's child nodes were changed in another Session " + parent.getQPath().getAsString()); } return childsCount; } /** * {@inheritDoc} */ public List<PropertyData> getChildPropertiesData(NodeData parent) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getChildPropertiesData(" + parent.getQPath().getAsString() + ") >>>>>"); } try { return (List<PropertyData>)mergeProps(parent, false, transactionableManager); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getChildPropertiesData(" + parent.getQPath().getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * {@inheritDoc} */ public List<PropertyData> getChildPropertiesData(NodeData parent, List<QPathEntryFilter> itemDataFilters) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getChildPropertiesData(" + parent.getQPath().getAsString() + ") >>>>>"); } try { List<PropertyData> childProperties = (isNew(parent.getIdentifier())) ? new ArrayList<PropertyData>() : transactionableManager .getChildPropertiesData(parent, itemDataFilters); return (List<PropertyData>)mergeProps(parent, childProperties, transactionableManager); } finally { if (LOG.isDebugEnabled()) { LOG.debug("getChildPropertiesData(" + parent.getQPath().getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * {@inheritDoc} */ public List<PropertyData> listChildPropertiesData(NodeData parent) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("listChildPropertiesData(" + parent.getQPath().getAsString() + ") >>>>>"); } try { return (List<PropertyData>)mergeProps(parent, true, transactionableManager); } finally { if (LOG.isDebugEnabled()) { LOG.debug("listChildPropertiesData(" + parent.getQPath().getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * Return the ACL of the location. A session pending changes will be searched too. Item path will * be traversed from the root node to a last existing item. * * @param path * - path of an ACL * @return - an item or its parent ancestor ACL * @throws RepositoryException */ public AccessControlList getACL(QPath path) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getACL(" + path.getAsString() + " ) >>>>>"); } try { NodeData parent = (NodeData)getItemData(Constants.ROOT_UUID); if (path.equals(Constants.ROOT_PATH)) { return parent.getACL(); } ItemData item = null; QPathEntry[] relPathEntries = path.getRelPath(path.getDepth()); for (int i = 0; i < relPathEntries.length; i++) { if (i == relPathEntries.length - 1) { item = getItemData(parent, relPathEntries[i], ItemType.NODE); } else { item = getItemData(parent, relPathEntries[i], ItemType.UNKNOWN); } if (item == null) { break; } if (item.isNode()) { parent = (NodeData)item; } else if (i < relPathEntries.length - 1) { throw new IllegalPathException("Get ACL. Path can not contains a property as the intermediate element"); } } if (item != null && item.isNode()) { // node ACL return ((NodeData)item).getACL(); } else { // item not found or it's a property - return parent ACL return parent.getACL(); } } finally { if (LOG.isDebugEnabled()) { LOG.debug("getACL(" + path.getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } public AccessControlList getACL(NodeData parent, QPathEntry name) throws RepositoryException { long start = 0; if (LOG.isDebugEnabled()) { start = System.currentTimeMillis(); LOG.debug("getACL(" + parent.getQPath().getAsString() + " + " + name.getAsString() + " ) >>>>>"); } try { ItemData item = getItemData(parent, name, ItemType.NODE); if (item != null && item.isNode()) { // node ACL return ((NodeData)item).getACL(); } else { // item not found or it's a property - return parent ACL return parent.getACL(); } } finally { if (LOG.isDebugEnabled()) { LOG.debug("getACL(" + parent.getQPath().getAsString() + " + " + name.getAsString() + ") <<<<< " + ((System.currentTimeMillis() - start) / 1000d) + "sec"); } } } /** * Reloads item in pool. * * @param data * item data * @return * @throws RepositoryException */ ItemImpl reloadItem(ItemData data) throws RepositoryException { return itemsPool.reload(data); } public void move(NodeData srcData, ItemDataMoveVisitor initializer) throws RepositoryException { srcData.accept(initializer); changesLog.addAll(initializer.getAllStates()); // reload items pool reloadItems(initializer); } public void reloadItems(ItemDataMoveVisitor initializer) throws RepositoryException { List<ItemState> states = initializer.getItemAddStates(); for (int i = 0, length = states.size(); i < length; i++) { ItemState state = states.get(i); if (state.isUpdated() || state.isRenamed()) { ItemImpl item = reloadItem(state.getData()); if (item != null) { invalidated.add(item); } } else if (state.isPathChanged()) { reloadDescendants(state.getOldPath(), state.getData().getQPath()); } } } /** * Traverses all the descendants of incoming item and creates DELETED state for them Adds DELETED * incoming state of incoming and descendants to the changes log and removes corresponding items * from pool (if any) * * @param itemData * - incoming state * @throws RepositoryException */ public void delete(ItemData itemData) throws RepositoryException { delete(itemData, itemData.getQPath(), false); } public void delete(ItemData itemData, QPath ancestorToSave) throws RepositoryException { delete(itemData, ancestorToSave, false); } protected void delete(ItemData itemData, QPath ancestorToSave, boolean isInternall) throws RepositoryException { List<? extends ItemData> list = mergeList(itemData, transactionableManager, true, MERGE_ITEMS); List<ItemState> deletes = new ArrayList<ItemState>(); boolean fireEvent = !isNew(itemData.getIdentifier()); // if node mix:versionable vs will be removed from Item.remove method. boolean checkRemoveChildVersionStorages = false; if (itemData.isNode()) { checkRemoveChildVersionStorages = !session.getWorkspace().getNodeTypesHolder().isNodeType(Constants.NT_VERSIONHISTORY, ((NodeData)itemData).getPrimaryTypeName()); } boolean rootAdded = false; for (ItemData data : list) { if (data.equals(itemData)) { rootAdded = true; } deletes.add(new ItemState(data, ItemState.DELETED, fireEvent, ancestorToSave, isInternall)); // if subnode contains JCR_VERSIONHISTORY property // we should remove version storage manually if (checkRemoveChildVersionStorages && !data.isNode() && Constants.JCR_VERSIONHISTORY.equals(data.getQPath().getName())) { try { PropertyData vhPropertyData = (PropertyData)getItemData(data.getIdentifier()); removeVersionHistory(ValueDataUtil.getString(vhPropertyData.getValues().get(0)), null, ancestorToSave); } catch (IllegalStateException e) { throw new RepositoryException(e.getLocalizedMessage(), e); } } ItemImpl pooled = itemsPool.remove(data.getIdentifier()); if (pooled != null) { pooled.invalidate(); // invalidate immediate invalidated.add(pooled); } } // 4 add item itself if not added if (!rootAdded) { deletes.add(new ItemState(itemData, ItemState.DELETED, fireEvent, ancestorToSave, isInternall)); ItemImpl pooled = itemsPool.remove(itemData.getIdentifier()); if (pooled != null) { pooled.invalidate(); // invalidate immediate invalidated.add(pooled); } if (LOG.isDebugEnabled()) { LOG.debug("deleted top item: " + itemData.getQPath().getAsString()); } } // 6 sort items to delete Collections.sort(deletes, new PathSorter()); if (!fireEvent) { // 7 erase evenFire flag if it's a new item changesLog.eraseEventFire(itemData.getIdentifier()); } changesLog.addAll(deletes); // log.info(changesLog.dump()) if (itemData.isNode()) { // 8 reindex same-name siblings changesLog.addAll(reindexSameNameSiblings((NodeData)itemData, this)); } } /** * Check when it's a Node and is versionable will a version history removed. Case of last version * in version history. * * @throws RepositoryException * @throws ConstraintViolationException * @throws VersionException */ public void removeVersionHistory(String vhID, QPath containingHistory, QPath ancestorToSave) throws RepositoryException, ConstraintViolationException, VersionException { NodeData vhnode = (NodeData)getItemData(vhID); if (vhnode == null) { ItemState vhState = changesLog.getItemState(vhID); if (vhState != null && vhState.isDeleted()) { return; } LOG.debug("Version history is not found. UUID: " + vhID + ". Context item (ancestor to save) " + ancestorToSave.getAsString()); return; } // mix:versionable // we have to be sure that any versionable node somewhere in repository // doesn't refers to a VH of the node being deleted. RepositoryImpl rep = (RepositoryImpl)session.getRepository(); for (String wsName : rep.getWorkspaceNames()) { SessionImpl wsSession = session.getWorkspace().getName().equals(wsName) ? session : (SessionImpl)rep.getSystemSession(wsName); try { for (PropertyData sref : wsSession.getTransientNodesManager().getReferencesData(vhID, false)) { // Check if this VH isn't referenced from somewhere in workspace // or isn't contained in another one as a child history. // Ask ALL references incl. properties from version storage. if (sref.getQPath().isDescendantOf(Constants.JCR_VERSION_STORAGE_PATH)) { if (!sref.getQPath().isDescendantOf(vhnode.getQPath()) && (containingHistory != null ? !sref.getQPath().isDescendantOf(containingHistory) : true)) { // has a reference to the VH in version storage, // it's a REFERENCE property jcr:childVersionHistory of // nt:versionedChild // i.e. this VH is a child history in an another history. // We can't remove this VH now. return; } } else if (wsSession != session) // NOSONAR { // has a reference to the VH in traversed workspace, // it's not a version storage, i.e. it's a property of versionable // node somewhere in ws. // We can't remove this VH now. return; } // else -- if we has a references in workspace where the VH is being // deleted we can remove VH now. } } finally { if (wsSession != session) // NOSONAR { wsSession.logout(); } } } // remove child versions from VH (if found) ChildVersionRemoveVisitor cvremover = new ChildVersionRemoveVisitor(session.getTransientNodesManager(), session.getWorkspace().getNodeTypesHolder(), vhnode.getQPath(), ancestorToSave); vhnode.accept(cvremover); // remove VH delete(vhnode, ancestorToSave, true); } /** * Reindex same-name siblings of the node Reindex is actual for remove, move only. If node is * added then its index always is a last in list of childs. * * @param cause * a node caused reindexing, i.e. deleted or moved node. */ protected List<ItemState> reindexSameNameSiblings(NodeData cause, ItemDataConsumer dataManager) throws RepositoryException { List<ItemState> changes = new ArrayList<ItemState>(); NodeData parentNodeData = (NodeData)dataManager.getItemData(cause.getParentIdentifier()); NodeData nextSibling = (NodeData)dataManager.getItemData(parentNodeData, new QPathEntry(cause.getQPath().getName(), cause.getQPath() .getIndex() + 1), ItemType.NODE); String reindexedId = null; // repeat till next sibling exists and it's not a caused Node (deleted or moved to) or just // reindexed while (nextSibling != null && !nextSibling.getIdentifier().equals(cause.getIdentifier()) && !nextSibling.getIdentifier().equals(reindexedId)) { QPath siblingOldPath = QPath.makeChildPath(nextSibling.getQPath().makeParentPath(), nextSibling.getQPath().getName(), nextSibling .getQPath().getIndex()); // update with new index QPath siblingPath = QPath.makeChildPath(nextSibling.getQPath().makeParentPath(), nextSibling.getQPath().getName(), nextSibling .getQPath().getIndex() - 1); NodeData reindexed = new TransientNodeData(siblingPath, nextSibling.getIdentifier(), nextSibling.getPersistedVersion(), nextSibling.getPrimaryTypeName(), nextSibling.getMixinTypeNames(), nextSibling.getOrderNumber(), nextSibling.getParentIdentifier(), nextSibling.getACL()); reindexedId = reindexed.getIdentifier(); ItemState reindexedState = ItemState.createUpdatedState(reindexed); changes.add(reindexedState); itemsPool.reload(reindexed); reloadDescendants(siblingOldPath, siblingPath); // next... nextSibling = (NodeData)dataManager.getItemData(parentNodeData, new QPathEntry(nextSibling.getQPath().getName(), nextSibling.getQPath().getIndex() + 1), ItemType.NODE); } return changes; } /** * Updates (adds or modifies) item state in the session transient storage. * * @param itemState * - the state * @param pool * - if true Manager force pooling this State so next calling will returna the same * object Common rule: use pool = true if the Item supposed to be returned by JCR API * (Node.addNode(), Node.setProperty() for ex) (NOTE: independently of pooling the * Manager always return actual Item state) * @return * @throws RepositoryException */ public ItemImpl update(ItemState itemState, boolean pool) throws RepositoryException { if (itemState.isDeleted()) { throw new RepositoryException("Illegal state DELETED. Use delete(...) method"); } changesLog.add(itemState); return readItem(itemState.getData(), null, pool, false); } /** * Updates (adds or modifies) item state in the session transient storage without creation ItemImpl. * * @param itemState * - the state * @throws RepositoryException */ public void updateItemState(ItemState itemState) throws RepositoryException { if (itemState.isDeleted()) { throw new RepositoryException("Illegal state DELETED. Use delete(...) method"); } changesLog.add(itemState); } /** * Commit changes * * @param path * @throws RepositoryException * @throws AccessDeniedException * @throws ReferentialIntegrityException * @throws InvalidItemStateException */ public void commit(QPath path) throws RepositoryException, AccessDeniedException, ReferentialIntegrityException, InvalidItemStateException, ItemExistsException { // validate all, throw an exception if validation failed validate(path); PlainChangesLog cLog = changesLog.pushLog(path); if (LOG.isDebugEnabled()) { LOG.debug(" ----- commit -------- \n" + cLog.dump()); } try { transactionableManager.save(cLog); invalidated.clear(); } catch (AccessDeniedException e) { remainChangesBack(cLog); throw new AccessDeniedException(e); } catch (InvalidItemStateException e) { remainChangesBack(cLog); throw new InvalidItemStateException(e); } catch (ItemExistsException e) { remainChangesBack(cLog); throw new ItemExistsException(e); } catch (ReferentialIntegrityException e) { remainChangesBack(cLog); throw new ReferentialIntegrityException(e); } catch (RepositoryException e) { remainChangesBack(cLog); throw new RepositoryException(e); } } /** * Save changes log records back in the session changes log. * <p> * Case of Session.save error. * * @param cLog */ private void remainChangesBack(PlainChangesLog cLog) { changesLog.addAll(cLog.getAllStates()); if (LOG.isDebugEnabled()) { LOG.debug(" ----- rollback ----- \n" + cLog.dump()); } } /** * Returns all REFERENCE properties that refer to this node. * * @see org.exoplatform.services.jcr.dataflow.ItemDataConsumer#getReferencesData(String, boolean) */ public List<PropertyData> getReferencesData(String identifier, boolean skipVersionStorage) throws RepositoryException { List<PropertyData> persisted = transactionableManager.getReferencesData(identifier, skipVersionStorage); List<PropertyData> sessionTransient = new ArrayList<PropertyData>(); for (PropertyData p : persisted) { sessionTransient.add(p); } return sessionTransient; } /** * Validate all user created changes saves like access permeations, mandatory items, value * constraint. * * @param path * @throws RepositoryException * @throws AccessDeniedException * @throws ReferentialIntegrityException */ private void validate(QPath path) throws RepositoryException, AccessDeniedException, ReferentialIntegrityException { List<ItemState> changes = changesLog.getAllStates(); for (ItemState itemState : changes) { if (itemState.isInternallyCreated()) { // skip internally created if (itemState.isMixinChanged()) { // ...except of check of ACL correct size for internally created // items. // If no permissions in the list throw exception. if (itemState.isDescendantOf(path)) { if (((NodeData)itemState.getData()).getACL().getPermissionsSize() < 1) { throw new RepositoryException("Node " + itemState.getData().getQPath().getAsString() + " has wrong formed ACL."); } } validateMandatoryItem(itemState); } } else { if (itemState.isDescendantOf(path)) { validateAccessPermissions(itemState); validateMandatoryItem(itemState); } if (path.isDescendantOf(itemState.getAncestorToSave())) { throw new ConstraintViolationException(path.getAsString() + " is the same or descendant of either Session.move()'s destination or source node only " + path.getAsString()); } } } } /** * Validate ItemState for access permeations * * @param changedItem * @throws RepositoryException * @throws AccessDeniedException */ private void validateAccessPermissions(ItemState changedItem) throws RepositoryException, AccessDeniedException { if (changedItem.isAddedAutoCreatedNodes()) { validateAddNodePermission(changedItem); } else if (changedItem.isDeleted()) { validateRemoveAccessPermission(changedItem); } else if (changedItem.isMixinChanged()) { validateMixinChangedPermission(changedItem); } else { NodeData parent = (NodeData)getItemData(changedItem.getData().getParentIdentifier()); if (parent != null) { if (changedItem.getData().isNode()) { // add node if (changedItem.isAdded()) { if (!accessManager.hasPermission(parent.getACL(), new String[]{PermissionType.ADD_NODE}, session .getUserState().getIdentity())) { throw new AccessDeniedException("Access denied: ADD_NODE " + changedItem.getData().getQPath().getAsString() + " for: " + session.getUserID() + " item owner " + parent.getACL().getOwner()); } } } else if (changedItem.isAdded() || changedItem.isUpdated()) { // add or update property if (!accessManager.hasPermission(parent.getACL(), new String[]{PermissionType.SET_PROPERTY}, session .getUserState().getIdentity())) { throw new AccessDeniedException("Access denied: SET_PROPERTY " + changedItem.getData().getQPath().getAsString() + " for: " + session.getUserID() + " item owner " + parent.getACL().getOwner()); } } } // else - parent not found, deleted in this session or from another } } private void validateRemoveAccessPermission(ItemState changedItem) throws RepositoryException, AccessDeniedException { NodeData nodeData = null; // if changedItem is node - check its ACL, if property - check parent node ACL if (changedItem.isNode()) { nodeData = (NodeData)changedItem.getData(); } else { nodeData = (NodeData)getItemData(changedItem.getData().getParentIdentifier()); if (nodeData == null) { return; } } if (!accessManager.hasPermission(nodeData.getACL(), new String[]{PermissionType.REMOVE}, session.getUserState() .getIdentity())) { throw new AccessDeniedException("Access denied: REMOVE " + changedItem.getData().getQPath().getAsString() + " for: " + session.getUserID() + " item owner " + nodeData.getACL().getOwner()); } } private void validateAddNodePermission(ItemState changedItem) throws AccessDeniedException { if (!accessManager.hasPermission(((NodeData)changedItem.getData()).getACL(), new String[]{PermissionType.ADD_NODE}, session.getUserState().getIdentity())) { throw new AccessDeniedException("Access denied: ADD_NODE" + changedItem.getData().getQPath().getAsString() + " for: " + session.getUserID() + " item owner " + ((NodeData)changedItem.getData()).getACL().getOwner()); } } private void validateMixinChangedPermission(ItemState changedItem) throws AccessDeniedException { if (!accessManager.hasPermission(((NodeData)changedItem.getData()).getACL(), new String[]{PermissionType.SET_PROPERTY}, session.getUserState().getIdentity())) { throw new AccessDeniedException("Access denied: SET_PROPERTY" + changedItem.getData().getQPath().getAsString() + " for: " + session.getUserID() + " item owner " + ((NodeData)changedItem.getData()).getACL().getOwner()); } } /** * Validate ItemState which represents the add node, for it's all mandatory items * * @param changedItem * @throws ConstraintViolationException * @throws AccessDeniedException */ private void validateMandatoryItem(ItemState changedItem) throws ConstraintViolationException, AccessDeniedException { if (changedItem.getData().isNode() && (changedItem.isAdded() || changedItem.isMixinChanged()) && !changesLog.getItemState(changedItem.getData().getQPath()).isDeleted()) { // Node not in delete state. It might be a wrong if (!changesLog.getItemState(changedItem.getData().getIdentifier()).isDeleted()) { NodeData nData = (NodeData)changedItem.getData(); try { validateMandatoryChildren(nData); } catch (ConstraintViolationException e) { throw e; } catch (AccessDeniedException e) { throw e; } catch (RepositoryException e) { LOG.warn("Unexpected exception. Probable wrong data. Exception message:" + e.getLocalizedMessage()); } } } } public void validateMandatoryChildren(NodeData nData) throws ConstraintViolationException, AccessDeniedException, RepositoryException { Collection<ItemDefinitionData> mandatoryItemDefs = session.getWorkspace().getNodeTypesHolder().getManadatoryItemDefs(nData.getPrimaryTypeName(), nData.getMixinTypeNames()); for (ItemDefinitionData itemDefinitionData : mandatoryItemDefs) { ItemType itemType = itemDefinitionData.isNodeDefinition() ? ItemType.NODE : ItemType.PROPERTY; if (getItemData(nData, new QPathEntry(itemDefinitionData.getName(), 0), itemType) == null) { throw new ConstraintViolationException("Mandatory item " + itemDefinitionData.getName() + " not found. Node [" + nData.getQPath().getAsString() + " primary type: " + nData.getPrimaryTypeName().getAsString() + "]"); } } } /** * Removes all pending changes of this item * * @param item * @throws RepositoryException */ void rollback(ItemData item) throws InvalidItemStateException, RepositoryException { // remove from changes log (Session pending changes) PlainChangesLog slog = changesLog.pushLog(item.getQPath()); SessionChangesLog changes = new SessionChangesLog(slog.getAllStates(), session); for (Iterator<ItemImpl> removedIter = invalidated.iterator(); removedIter.hasNext();) { ItemImpl removed = removedIter.next(); QPath removedPath = removed.getLocation().getInternalPath(); ItemState rstate = changes.getItemState(removedPath); if (rstate != null) { if (rstate.isRenamed() || rstate.isPathChanged()) { // find DELETED rstate = changes.findItemState(rstate.getData().getIdentifier(), false, new int[]{ItemState.DELETED}); if (rstate == null) { continue; } } NodeData parent = (NodeData)transactionableManager.getItemData(rstate.getData().getParentIdentifier()); if (parent != null) { ItemData persisted = transactionableManager.getItemData(parent, rstate.getData().getQPath().getEntries()[rstate.getData() .getQPath().getEntries().length - 1], ItemType.getItemType(rstate.getData())); if (persisted != null) { // reload item data removed.loadData(persisted); } } // else it's transient item } else if (removed.getData() != null) { // No states for items left usecase ItemData persisted = transactionableManager.getItemData(removed.getData().getIdentifier()); if (persisted != null) { // reload item data removed.loadData(persisted); } } removedIter.remove(); } } /** * @see javax.jcr.Item#refresh(boolean) * @param item ItemData * @throws InvalidItemStateException * @throws RepositoryException */ void refresh(ItemData item) throws InvalidItemStateException, RepositoryException { if (!isModified(item) && itemsPool.contains(item.getIdentifier())) { // if not modified but was pooled, load data from persistent storage ItemData persisted = transactionableManager.getItemData(item.getIdentifier()); if (persisted == null) { // ...try by path NodeData parent = (NodeData)transactionableManager.getItemData(item.getParentIdentifier()); if (parent != null) { QPathEntry[] path = item.getQPath().getEntries(); persisted = transactionableManager.getItemData(parent, path[path.length - 1], ItemType.getItemType(item)); } // else, the item has an invalid state, will be throwed on save } if (persisted != null) { // the item itemsPool.reload(item.getIdentifier(), persisted); // the childs is acquired in the session. for (ItemImpl pooled : itemsPool.getDescendats(persisted.getQPath())) { persisted = transactionableManager.getItemData(pooled.getInternalIdentifier()); if (persisted == null) { // ...try by path NodeData parent = (NodeData)transactionableManager.getItemData(pooled.getParentIdentifier()); if (parent != null) { QPathEntry[] path = pooled.getData().getQPath().getEntries(); persisted = transactionableManager.getItemData(parent, path[path.length - 1], ItemType.getItemType(pooled .getData())); } // else, the item has an invalid state, will be throwed on save } if (persisted != null) { itemsPool.reload(pooled.getInternalIdentifier(), persisted); } } } else { throw new InvalidItemStateException( "An item is transient only or removed (either by this session or another) " + session.getLocationFactory().createJCRPath(item.getQPath()).getAsString(false)); } } } // for testing only protected ItemReferencePool getItemsPool() { return this.itemsPool; } /** * @return sessionChangesLog */ protected SessionChangesLog getChangesLog() { return this.changesLog; } /** * Merges incoming node with changes stored in this log i.e: 1. incoming data still not modified * if there are no corresponding changes 2. incoming data is refreshed with corresponding changes * if any 3. new data is added from changes 4. if changed data is marked as "deleted" it removes * from outgoing list WARN. THIS METHOD HAS SIBLING - mergeList, see below. * * @param rootData * @param persistChildNodes persisted child nodes, that will be merged with transient data * * @return merged nodes list */ protected List<? extends ItemData> mergeNodes(ItemData rootData, List<NodeData> persistChildNodes) throws RepositoryException { // 1 get all transient descendants Collection<ItemState> transientDescendants = changesLog.getLastChildrenStates(rootData, true); if (!transientDescendants.isEmpty()) { // 2 get ALL persisted descendants Map<String, ItemData> descendants = new LinkedHashMap<String, ItemData>(); for (int i = 0, length = persistChildNodes.size(); i < length; i++) { NodeData childNode = persistChildNodes.get(i); descendants.put(childNode.getIdentifier(), childNode); } // merge data for (ItemState state : transientDescendants) { ItemData data = state.getData(); if (!state.isDeleted()) { descendants.put(data.getIdentifier(), data); } else { descendants.remove(data.getIdentifier()); } } Collection<ItemData> desc = descendants.values(); return new ArrayList<ItemData>(desc); } else { return persistChildNodes; } } /** * Merges incoming node with changes stored in this log i.e: 1. incoming data still not modified * if there are no corresponding changes 2. incoming data is refreshed with corresponding changes * if any 3. new datas is added from changes 4. if chaged data is marked as "deleted" it removes * from outgoing list WARN. THIS METHOD HAS SIBLING - mergeList, see below. * * @param rootData * @return */ protected List<? extends ItemData> mergeNodes(ItemData rootData, DataManager dataManager) throws RepositoryException { // 1 get all transient descendants Collection<ItemState> transientDescendants = changesLog.getLastChildrenStates(rootData, true); if (!transientDescendants.isEmpty()) { // 2 get ALL persisted descendants Map<String, ItemData> descendants = new LinkedHashMap<String, ItemData>(); traverseStoredDescendants(rootData, dataManager, MERGE_NODES, descendants, false, transientDescendants); // merge data for (ItemState state : transientDescendants) { ItemData data = state.getData(); if (!state.isDeleted()) { descendants.put(data.getIdentifier(), data); } else { descendants.remove(data.getIdentifier()); } } Collection<ItemData> desc = descendants.values(); return new ArrayList<ItemData>(desc); } else { if (isNew(rootData.getIdentifier())) { return Collections.emptyList(); } return dataManager.getChildNodesData((NodeData)rootData); } } /** * Merges incoming property data with changes stored in this log i.e: 1. incoming data still not modified * if there are no corresponding changes 2. incoming data is refreshed with corresponding changes * if any 3. new datas is added from changes 4. if chaged data is marked as "deleted" it removes * from outgoing list WARN. THIS METHOD HAS SIBLING - mergeList, see below. * * @param rootData * @param childProperties * @param dataManager * @return */ protected List<? extends ItemData> mergeProps(ItemData rootData, List<PropertyData> childProperties, DataManager dataManager) throws RepositoryException { // 1 get all transient descendants Collection<ItemState> transientDescendants = changesLog.getLastChildrenStates(rootData, false); if (!transientDescendants.isEmpty()) { Map<String, ItemData> descendants = new LinkedHashMap<String, ItemData>(); outer : for (int i = 0, length = childProperties.size(); i < length; i++) { ItemData childProp = childProperties.get(i); for (ItemState transientState : transientDescendants) { if (!transientState.isNode() && !transientState.isDeleted() && transientState.getData().getQPath().getDepth() == childProp.getQPath().getDepth() && transientState.getData().getQPath().getName().equals(childProp.getQPath().getName())) { continue outer; } } descendants.put(childProp.getIdentifier(), childProp); } // merge data for (ItemState state : transientDescendants) { ItemData data = state.getData(); if (!state.isDeleted()) { descendants.put(data.getIdentifier(), data); } else { descendants.remove(data.getIdentifier()); } } Collection<ItemData> desc = descendants.values(); return new ArrayList<ItemData>(desc); } else { return childProperties; } } /** * Merges incoming property data with changes stored in this log i.e: 1. incoming data still not modified * if there are no corresponding changes 2. incoming data is refreshed with corresponding changes * if any 3. new datas is added from changes 4. if chaged data is marked as "deleted" it removes * from outgoing list WARN. THIS METHOD HAS SIBLING - mergeList, see below. * * @param rootData * @param listOnly * @return */ protected List<? extends ItemData> mergeProps(ItemData rootData, boolean listOnly, DataManager dataManager) throws RepositoryException { // 1 get all transient descendants Collection<ItemState> transientDescendants = changesLog.getLastChildrenStates(rootData, false); if (!transientDescendants.isEmpty()) { // 2 get ALL persisted descendants Map<String, ItemData> descendants = new LinkedHashMap<String, ItemData>(); traverseStoredDescendants(rootData, dataManager, MERGE_PROPS, descendants, listOnly, transientDescendants); // merge data for (ItemState state : transientDescendants) { ItemData data = state.getData(); if (!state.isDeleted()) { descendants.put(data.getIdentifier(), data); } else { descendants.remove(data.getIdentifier()); } } Collection<ItemData> desc = descendants.values(); return new ArrayList<ItemData>(desc); } else { return dataManager.getChildPropertiesData((NodeData)rootData); } } /** * Merge a list of nodes and properties of root data. NOTE. Properties in the list will have empty * value data. I.e. for operations not changes properties content. USED FOR DELETE. * * @param rootData * @param dataManager * @param deep * @param action * @return * @throws RepositoryException */ protected List<? extends ItemData> mergeList(ItemData rootData, DataManager dataManager, boolean deep, int action) throws RepositoryException { // 1 get all transient descendants List<ItemState> transientDescendants = new ArrayList<ItemState>(); traverseTransientDescendants(rootData, action, transientDescendants); if (deep || !transientDescendants.isEmpty()) { // 2 get ALL persisted descendants Map<String, ItemData> descendants = new LinkedHashMap<String, ItemData>(); traverseStoredDescendants(rootData, dataManager, action, descendants, true, transientDescendants); // merge data for (ItemState state : transientDescendants) { ItemData data = state.getData(); if (!state.isDeleted()) { descendants.put(data.getIdentifier(), data); } else { descendants.remove(data.getIdentifier()); } } Collection<ItemData> desc = descendants.values(); List<ItemData> retval; if (deep) { int size = desc.size(); retval = new ArrayList<ItemData>(size < 10 ? 10 : size); for (ItemData itemData : desc) { retval.add(itemData); if (deep && itemData.isNode()) { retval.addAll(mergeList(itemData, dataManager, true, action)); } } } else { retval = new ArrayList<ItemData>(desc); } return retval; } else { return getStoredDescendants(rootData, dataManager, action); } } /** * Calculate all stored descendants for the given parent node * * @param parent * @param dataManager * @param action * @param ret * @throws RepositoryException */ private void traverseStoredDescendants(ItemData parent, DataManager dataManager, int action, Map<String, ItemData> ret, boolean listOnly, Collection<ItemState> transientDescendants) throws RepositoryException { if (parent.isNode() && !isNew(parent.getIdentifier())) { if (action != MERGE_PROPS) { List<NodeData> childNodes = dataManager.getChildNodesData((NodeData)parent); for (int i = 0, length = childNodes.size(); i < length; i++) { NodeData childNode = childNodes.get(i); ret.put(childNode.getIdentifier(), childNode); } } if (action != MERGE_NODES) { List<PropertyData> childProps = listOnly ? dataManager.listChildPropertiesData((NodeData)parent) : dataManager .getChildPropertiesData((NodeData)parent); outer : for (int i = 0, length = childProps.size(); i < length; i++) { PropertyData childProp = childProps.get(i); for (ItemState transientState : transientDescendants) { if (!transientState.isNode() && !transientState.isDeleted() && transientState.getData().getQPath().getDepth() == childProp.getQPath().getDepth() && transientState.getData().getQPath().getName().equals(childProp.getQPath().getName())) { continue outer; } } if (!childProp.getQPath().isDescendantOf(parent.getQPath(), true)) { // In case we get the data from the cache, we need to set the correct path QPath qpath = QPath.makeChildPath(parent.getQPath(), childProp.getQPath().getName()); childProp = new PersistedPropertyData(childProp.getIdentifier(), qpath, childProp.getParentIdentifier(), childProp.getPersistedVersion(), childProp.getType(), childProp.isMultiValued(), childProp.getValues(), new SimplePersistedSize( ((PersistedPropertyData)childProp).getPersistedSize())); } ret.put(childProp.getIdentifier(), childProp); } } } } /** * Get all stored descendants for the given parent node * * @param parent * @param dataManager * @param action * @throws RepositoryException */ private List<? extends ItemData> getStoredDescendants(ItemData parent, DataManager dataManager, int action) throws RepositoryException { if (parent.isNode()) { List<ItemData> childItems = null; List<NodeData> childNodes = dataManager.getChildNodesData((NodeData)parent); if (action != MERGE_NODES) { childItems = new ArrayList<ItemData>(childNodes); } else { return childNodes; } List<PropertyData> childProps = dataManager.getChildPropertiesData((NodeData)parent); if (action != MERGE_PROPS) { childItems.addAll(childProps); } else { return childProps; } return childItems; } return null; } /** * Calculate all transient descendants for the given parent node * * @param parent * @param action * @param ret * @throws RepositoryException */ private void traverseTransientDescendants(ItemData parent, int action, List<ItemState> ret) throws RepositoryException { if (parent.isNode()) { if (action != MERGE_PROPS) { Collection<ItemState> childNodes = changesLog.getLastChildrenStates(parent, true); for (ItemState childNode : childNodes) { ret.add(childNode); } } if (action != MERGE_NODES) { Collection<ItemState> childProps = changesLog.getLastChildrenStates(parent, false); for (ItemState childProp : childProps) { ret.add(childProp); } } } } /** * Reload item's descendants in item reference pool * * @param parentOld old item's QPath to get descendants out of item reference pool * @param parent new item's QPath to set for reloaded descendants * @throws RepositoryException */ private void reloadDescendants(QPath parentOld, QPath parent) throws RepositoryException { List<ItemImpl> items = itemsPool.getDescendats(parentOld); for (ItemImpl item : items) { ItemData oldItemData = item.getData(); ItemData newItemData = updatePath(parentOld, parent, oldItemData); ItemImpl reloadedItem = reloadItem(newItemData); if (reloadedItem != null) { invalidated.add(reloadedItem); } } } /** * Updates the path if needed and gives the updated item data if an update was needed or the provided item data * otherwise * @throws IllegalPathException */ private ItemData updatePathIfNeeded(ItemData data) throws IllegalPathException { if (data == null || changesLog.getAllPathsChanged() == null) return data; List<ItemState> states = changesLog.getAllPathsChanged(); for (int i = 0, length = states.size(); i < length; i++) { ItemState state = states.get(i); if (data.getQPath().isDescendantOf(state.getOldPath())) { data = updatePath(state.getOldPath(), state.getData().getQPath(), data); } } return data; } /** * Updates the path of the item data and gives the updated objects * @param parentOld the path of the old ancestor * @param parent the path of the new ancestor * @param oldItemData the old item data * @return the {@link ItemData} with the updated path * @throws IllegalPathException if the relative path could not be retrieved */ private ItemData updatePath(QPath parentOld, QPath parent, ItemData oldItemData) throws IllegalPathException { int relativeDegree = oldItemData.getQPath().getDepth() - parentOld.getDepth(); QPath newQPath = QPath.makeChildPath(parent, oldItemData.getQPath().getRelPath(relativeDegree)); ItemData newItemData; if (oldItemData.isNode()) { NodeData oldNodeData = (NodeData)oldItemData; newItemData = new TransientNodeData(newQPath, oldNodeData.getIdentifier(), oldNodeData.getPersistedVersion(), oldNodeData.getPrimaryTypeName(), oldNodeData.getMixinTypeNames(), oldNodeData.getOrderNumber(), oldNodeData.getParentIdentifier(), oldNodeData.getACL()); } else { PropertyData oldPropertyData = (PropertyData)oldItemData; newItemData = new TransientPropertyData(newQPath, oldPropertyData.getIdentifier(), oldPropertyData.getPersistedVersion(), oldPropertyData.getType(), oldPropertyData.getParentIdentifier(), oldPropertyData.isMultiValued(), oldPropertyData.getValues()); } return newItemData; } /** * Pool for touched items. */ protected final class ItemReferencePool { private WeakValueHashMap<String, ItemImpl> items; ItemReferencePool() { items = new WeakValueHashMap<String, ItemImpl>(); } ItemImpl remove(String identifier) { ItemImpl item = items.remove(identifier); return item; } Collection<ItemImpl> getAll() { List<ItemImpl> list = new ArrayList<ItemImpl>(); for (ItemImpl item : items.values()) { if (item != null) { list.add(item); } } return list; } int size() { return items.size(); } /** * @param identifier * @return true if item with given identifier is pooled * @throws RepositoryException */ boolean contains(String identifier) { return items.containsKey(identifier); } /** * Get ItemImpl from the pool using given data. * * @param newData ItemData * @return ItemImpl item * @throws RepositoryException */ ItemImpl get(final ItemData newData) throws RepositoryException { return get(newData, null); } /** * Get ItemImpl from the pool using given data. * * @param newData ItemData * @param parent nodeData * @return ItemImpl item * @throws RepositoryException */ ItemImpl get(final ItemData newData, final NodeData parent) throws RepositoryException { final String identifier = newData.getIdentifier(); ItemImpl item = items.get(identifier); if (item != null) { item.loadData(newData, parent); } else { item = itemFactory.createItem(newData, parent); items.put(item.getInternalIdentifier(), item); } return item; } /** * Reload an existed item in the pool with given data * * @param itemData * - given data * @return an existed item of null if no item is pooled with a given data Identifier * @throws RepositoryException */ ItemImpl reload(ItemData itemData) throws RepositoryException { return reload(itemData.getIdentifier(), itemData); } ItemImpl reload(String identifier, ItemData newItemData) throws RepositoryException { ItemImpl item = items.get(identifier); if (item != null) { item.loadData(newItemData); return item; } return null; } /** * Search for all descendants of given parent path. * * @parentPath parent path * @return - List of ItemImpl */ List<ItemImpl> getDescendats(QPath parentPath) { List<ItemImpl> desc = new ArrayList<ItemImpl>(); Collection<ItemImpl> snapshort = getAll(); for (ItemImpl pitem : snapshort) { if (pitem != null) { if (pitem.getData().getQPath().isDescendantOf(parentPath)) { desc.add(pitem); } } } return desc; } String dump() { StringBuilder str = new StringBuilder("Items Pool: \n"); try { for (ItemImpl item : getAll()) { if (item != null) { str.append(item.isNode() ? "Node\t\t" : "Property\t"); str.append("\t").append(item.isValid()); str.append("\t").append(item.isNew()); str.append("\t").append(item.getInternalIdentifier()); str.append("\t").append(item.getPath()).append("\n"); } } } catch (RepositoryException e) { LOG.error(e.getLocalizedMessage(), e); } return str.toString(); } } /** * Class creates the Item from ItemData; */ private class SessionItemFactory { private ItemImpl createItem(ItemData data, NodeData parent) throws RepositoryException { if (data.isNode()) { return createNode((NodeData)data, parent); } else { return createProperty(data, parent); } } private NodeImpl createNode(NodeData data, NodeData parent) throws RepositoryException { NodeImpl node = new NodeImpl(data, parent, session); if (data.getPrimaryTypeName().equals(Constants.NT_VERSION)) { return new VersionImpl(data, session); } else if (data.getPrimaryTypeName().equals(Constants.NT_VERSIONHISTORY)) { return new VersionHistoryImpl(data, session); } else { return node; } } private PropertyImpl createProperty(ItemData data, NodeData parent) throws RepositoryException { return new PropertyImpl(data, parent, session); } } /** * Class helps on to sort nodes on deleting */ private class PathSorter implements Comparator<ItemState> { public int compare(final ItemState i1, final ItemState i2) { return -i1.getData().getQPath().compareTo(i2.getData().getQPath()); } } }