/* * 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.dataflow; import org.exoplatform.services.jcr.core.ExtendedSession; 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.QPath; import org.exoplatform.services.jcr.datamodel.QPathEntry; import org.exoplatform.services.jcr.impl.Constants; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by The eXo Platform SAS. * * @author Gennady Azarenkov * @version $Id$ * Stores collection of ItemStates */ public class PlainChangesLogImpl implements Externalizable, PlainChangesLog { /** * Constant null value */ private static final int NULL_VALUE = -1; /** * Constant not null value */ private static final int NOT_NULL_VALUE = 1; /** * Constant serial Version UID */ private static final long serialVersionUID = 5624550860372364084L; /** * Collect of all item */ protected List<ItemState> items; /** * Value session id */ protected String sessionId; /** * Value event type */ protected int eventType; /** * Value session */ protected ExtendedSession session; /** * ItemState index storage. Used in getItemState() by id and path. */ protected Map<Object, ItemState> index = new HashMap<Object, ItemState>(); /** * ItemState index storage. Used to store last child nodes states. */ protected Map<String, Map<String, ItemState>> lastChildNodeStates = new HashMap<String, Map<String, ItemState>>(); /** * ItemState index storage. Used to store last child properties states. */ protected Map<String, Map<String, ItemState>> lastChildPropertyStates = new HashMap<String, Map<String, ItemState>>(); /** * ItemState index storage. Used to store child nodes states. */ protected Map<String, List<ItemState>> childNodeStates = new HashMap<String, List<ItemState>>(); /** * ItemState index storage. Used to store child properties states. */ protected Map<String, List<ItemState>> childPropertyStates = new HashMap<String, List<ItemState>>(); /** * Stores info for persisted child nodes by parent identifier. * <br>Index in array points to: * <br>0 - child nodes count. * <br>1 - last child order number */ protected Map<String, int[]> childNodesInfo = new HashMap<String, int[]>(); /** * The list of states corresponding to path changed */ protected List<ItemState> allPathsChanged; /** * Index in <code>childNodesInfo</code> value array to store child nodes count. */ protected final int CHILD_NODES_COUNT = 0; /** * Index in <code>childNodesInfo</code> value array to store last child order number. */ protected final int CHILD_NODES_LAST_ORDER_NUMBER = 1; /** * Identifier of system and non-system logs pair. Null if no pair found. */ protected String pairId; /** * Constructor. * * @param items List of ItemState * @param session Session * @param eventType int */ public PlainChangesLogImpl(List<ItemState> items, ExtendedSession session, int eventType) { this(items, session.getId(), eventType, null, session); } /** * Constructor. * * @param items List of ItemState * @param sessionId String * @param eventType int */ public PlainChangesLogImpl(List<ItemState> items, String sessionId, int eventType) { this(items, sessionId, eventType, null, null); } /** * Constructor with undefined event type. * * @param items List of ItemState * @param session Session */ public PlainChangesLogImpl(List<ItemState> items, ExtendedSession session) { this(items, session, -1); } /** * PlainChangesLogImpl constructor with an empty log. * * @param session Session */ public PlainChangesLogImpl(ExtendedSession session) { this(new ArrayList<ItemState>(), session); } /** * PlainChangesLogImpl constructor with an empty log. * * @param sessionId String */ public PlainChangesLogImpl(String sessionId) { this(new ArrayList<ItemState>(), sessionId, -1); } /** * Default PlainChangesLogImpl constructor with an empty log. constructor (for externalizable mainly) */ public PlainChangesLogImpl() { this(new ArrayList<ItemState>(), (String)null, -1); } /** * {@inheritDoc} */ public List<ItemState> getAllStates() { return items; } /** * {@inheritDoc} */ public int getSize() { return items.size(); } /** * {@inheritDoc} */ public int getEventType() { return eventType; } /** * {@inheritDoc} */ public String getSessionId() { return sessionId; } /** * {@inheritDoc} */ public ExtendedSession getSession() { return session; } /** * {@inheritDoc} */ public PlainChangesLog add(ItemState change) { items.add(change); addItem(change); return this; } /** * {@inheritDoc} */ public PlainChangesLog addAll(List<ItemState> changes) { items.addAll(changes); for (int i = 0, length = changes.size(); i < length; i++) { addItem(changes.get(i)); } return this; } /** * {@inheritDoc} */ public void clear() { items.clear(); index.clear(); lastChildNodeStates.clear(); lastChildPropertyStates.clear(); childNodeStates.clear(); childPropertyStates.clear(); childNodesInfo.clear(); allPathsChanged = null; } /** * {@inheritDoc} */ public String getPairId() { return pairId; } /** * {@inheritDoc} */ public String dump() { StringBuilder str = new StringBuilder("ChangesLog: \n"); for (int i = 0; i < items.size(); i++) { str.append(" ").append(ItemState.nameFromValue(items.get(i).getState())).append("\t") .append(items.get(i).getData().getIdentifier()); str.append("\t").append("isPersisted=").append(items.get(i).isPersisted()).append("\t").append("isEventFire="); str.append(items.get(i).isEventFire()).append("\t").append("isInternallyCreated=") .append(items.get(i).isInternallyCreated()).append("\t"); str.append(items.get(i).getData().getQPath().getAsString()).append("\n"); } return str.toString(); } /** * Full qualified constructor. * * @param items List of ItemState * @param sessionId String * @param eventType int * @param pairId String */ protected PlainChangesLogImpl(List<ItemState> items, String sessionId, int eventType, String pairId, ExtendedSession session) { this.session = session; this.sessionId = sessionId; this.eventType = eventType; this.pairId = pairId; this.items = new ArrayList<ItemState>(); addAll(items); } /** * Creates a new instance of {@link PlainChangesLogImpl} by copying metadata from originalLog * instance with Items provided. * * @param items * @param originalLog * @return */ public static PlainChangesLogImpl createCopy(List<ItemState> items, PlainChangesLog originalLog) { return createCopy(items, originalLog.getPairId(), originalLog); } /** * Creates a new instance of {@link PlainChangesLogImpl} by copying metadata from originalLog * instance with Items and PairID provided. Metadata will be copied excluding PairID. * * @param items * @param originalLog * @return */ public static PlainChangesLogImpl createCopy(List<ItemState> items, String pairId, PlainChangesLog originalLog) { if (originalLog.getSession() != null) { return new PlainChangesLogImpl(items, originalLog.getSession().getId(), originalLog.getEventType(), pairId, originalLog.getSession()); } return new PlainChangesLogImpl(items, originalLog.getSessionId(), originalLog.getEventType(), pairId, null); } /** * Removes the property or node and all descendants from the log * * @param item * item */ public void remove(ItemState item) { if (item.isNode()) { remove(item.getData().getQPath()); } else { removeProperty(item, -1); } } /** * Adds item to the changes log. * * @param item * the item */ protected void addItem(ItemState item) { index.put(item.getData().getIdentifier(), item); index.put(item.getData().getQPath(), item); index.put(new ParentIDQPathBasedKey(item), item); index.put(new IDStateBasedKey(item.getData().getIdentifier(), item.getState()), item); if (item.getData().isNode()) { Map<String, ItemState> children = lastChildNodeStates.get(item.getData().getParentIdentifier()); if (children == null) { children = new HashMap<String, ItemState>(); lastChildNodeStates.put(item.getData().getParentIdentifier(), children); } children.put(item.getData().getIdentifier(), item); List<ItemState> listItemState = childNodeStates.get(item.getData().getParentIdentifier()); if (listItemState == null) { listItemState = new ArrayList<ItemState>(); childNodeStates.put(item.getData().getParentIdentifier(), listItemState); } listItemState.add(item); if (item.isPathChanged()) { if (allPathsChanged == null) { allPathsChanged = new ArrayList<ItemState>(); } allPathsChanged.add(item); } } else { Map<String, ItemState> children = lastChildPropertyStates.get(item.getData().getParentIdentifier()); if (children == null) { children = new HashMap<String, ItemState>(); lastChildPropertyStates.put(item.getData().getParentIdentifier(), children); } children.put(item.getData().getIdentifier(), item); List<ItemState> listItemState = childPropertyStates.get(item.getData().getParentIdentifier()); if (listItemState == null) { listItemState = new ArrayList<ItemState>(); childPropertyStates.put(item.getData().getParentIdentifier(), listItemState); } listItemState.add(item); } if (item.isNode() && item.isPersisted()) { int[] childInfo = childNodesInfo.get(item.getData().getParentIdentifier()); if (childInfo == null) { childInfo = new int[2]; } if (item.isDeleted()) { --childInfo[CHILD_NODES_COUNT]; } else if (item.isAdded()) { ++childInfo[CHILD_NODES_COUNT]; childInfo[CHILD_NODES_LAST_ORDER_NUMBER] = ((NodeData)item.getData()).getOrderNumber(); } childNodesInfo.put(item.getData().getParentIdentifier(), childInfo); } } /** * @return the allPathsChanged */ public List<ItemState> getAllPathsChanged() { return allPathsChanged; } public int getChildNodesCount(String rootIdentifier) { int[] childInfo = childNodesInfo.get(rootIdentifier); return childInfo == null ? 0 : childInfo[CHILD_NODES_COUNT]; } public int getLastChildOrderNumber(String rootIdentifier) { int[] childInfo = childNodesInfo.get(rootIdentifier); return childInfo == null ? -1 : childInfo[CHILD_NODES_LAST_ORDER_NUMBER]; } /** * Removes the item at the rootPath and all descendants from the log * * @param rootPath * path */ public void remove(QPath rootPath) { for (int i = items.size() - 1; i >= 0; i--) { ItemState item = items.get(i); QPath qPath = item.getData().getQPath(); if (qPath.isDescendantOf(rootPath) || item.getAncestorToSave().isDescendantOf(rootPath) || item.getAncestorToSave().equals(rootPath) || qPath.equals(rootPath)) { if (item.isNode()) { removeNode(item, i); } else { removeProperty(item, i); } } } } /** * Removes the node from the log * * @param item * ItemState */ private void removeNode(ItemState item, int indexItem) { items.remove(indexItem); index.remove(item.getData().getIdentifier()); index.remove(item.getData().getQPath()); index.remove(new ParentIDQPathBasedKey(item)); index.remove(new IDStateBasedKey(item.getData().getIdentifier(), item.getState())); childNodesInfo.remove(item.getData().getIdentifier()); lastChildNodeStates.remove(item.getData().getIdentifier()); childNodeStates.remove(item.getData().getIdentifier()); if (allPathsChanged != null && item.isPathChanged()) { allPathsChanged.remove(item); if (allPathsChanged.isEmpty()) allPathsChanged = null; } if (item.isPersisted()) { int childInfo[] = childNodesInfo.get(item.getData().getParentIdentifier()); if (childInfo != null) { if (item.isDeleted()) { ++childInfo[CHILD_NODES_COUNT]; } else if (item.isAdded()) { --childInfo[CHILD_NODES_COUNT]; } childNodesInfo.put(item.getData().getParentIdentifier(), childInfo); } } Map<String, ItemState> children = lastChildNodeStates.get(item.getData().getParentIdentifier()); if (children != null) { children.remove(item.getData().getIdentifier()); if (children.isEmpty()) { lastChildNodeStates.remove(item.getData().getParentIdentifier()); } } List<ItemState> listItemStates = childNodeStates.get(item.getData().getParentIdentifier()); if (listItemStates != null) { listItemStates.remove(item); if (listItemStates.isEmpty()) { childNodeStates.remove(item.getData().getParentIdentifier()); } } if ((children == null || children.isEmpty()) && (listItemStates == null || listItemStates.isEmpty())) { childNodesInfo.remove(item.getData().getParentIdentifier()); } } /** * Removes the property from the log * * @param item * ItemState */ private void removeProperty(ItemState item, int indexItem) { if (indexItem == -1) { items.remove(item); } else { items.remove(indexItem); } index.remove(item.getData().getIdentifier()); index.remove(item.getData().getQPath()); index.remove(new ParentIDQPathBasedKey(item)); index.remove(new IDStateBasedKey(item.getData().getIdentifier(), item.getState())); lastChildPropertyStates.remove(item.getData().getIdentifier()); childPropertyStates.remove(item.getData().getIdentifier()); Map<String, ItemState> children = lastChildPropertyStates.get(item.getData().getParentIdentifier()); if (children != null) { children.remove(item.getData().getIdentifier()); if (children.isEmpty()) { lastChildPropertyStates.remove(item.getData().getParentIdentifier()); } } List<ItemState> listItemStates = childPropertyStates.get(item.getData().getParentIdentifier()); if (listItemStates != null) { listItemStates.remove(item); if (listItemStates.isEmpty()) { childPropertyStates.remove(item.getData().getParentIdentifier()); } } } /** * Collect last in ChangesLog order item child changes. * * @param rootData * - a item root of the changes scan * @param forNodes * retrieves nodes' ItemStates is true, or properties' otherwise * @return child items states */ public Collection<ItemState> getLastChildrenStates(ItemData rootData, boolean forNodes) { Map<String, ItemState> children = forNodes ? lastChildNodeStates.get(rootData.getIdentifier()) : lastChildPropertyStates.get(rootData .getIdentifier()); return children == null ? new ArrayList<ItemState>() : children.values(); } /** * Return the last item state from ChangesLog. * * @param item * an item data the last state which need to be taken * @param forNode * retrieves nodes' ItemStates is true, or properties' otherwise * @return the last item state */ public ItemState getLastState(ItemData item, boolean forNode) { Map<String, ItemState> children = forNode ? lastChildNodeStates.get(item.getParentIdentifier()) : lastChildPropertyStates.get(item .getParentIdentifier()); return children == null ? null : children.get(item.getIdentifier()); } /** * Collect changes of all item direct childs. Including the item itself. * @param rootIdentifier root identifier * @param forNodes must be returned nodes or properties * @return Collect changes of all item direct childs */ public List<ItemState> getChildrenChanges(String rootIdentifier, boolean forNodes) { List<ItemState> children = forNodes ? childNodeStates.get(rootIdentifier) : childPropertyStates.get(rootIdentifier); return children == null ? new ArrayList<ItemState>() : children; } /** * Get ItemState by identifier and state. * * NOTE: Uses index HashMap. * * @param itemIdentifier * @param state * @return */ public ItemState getItemState(String itemIdentifier, int state) { return index.get(new IDStateBasedKey(itemIdentifier, state)); } /** * Get ItemState by identifier * @param itemIdentifier * @return */ public ItemState getItemState(String itemIdentifier) { return index.get(itemIdentifier); } /** * Get ItemState by absolute path. * * NOTE: Uses index HashMap. * * @param itemPath * @return */ public ItemState getItemState(QPath itemPath) { return index.get(itemPath); } /** * Get ItemState by parent and item name. * * @param parentData * parent * @param name * item name * @param itemType * item type * @return * @throws IllegalPathException */ public ItemState getItemState(NodeData parentData, QPathEntry name, ItemType itemType) throws IllegalPathException { if (itemType != ItemType.UNKNOWN) { return index.get(new ParentIDQPathBasedKey(parentData.getIdentifier(), name, itemType)); } else { ItemState state = index.get(new ParentIDQPathBasedKey(parentData.getIdentifier(), name, ItemType.NODE)); if (state == null) { state = index.get(new ParentIDQPathBasedKey(parentData.getIdentifier(), name, ItemType.PROPERTY)); } return state; } } /** * @param rootPath * @return item state at the rootPath and its descendants */ public List<ItemState> getDescendantsChanges(QPath rootPath) { List<ItemState> list = new ArrayList<ItemState>(); if (rootPath.equals(Constants.ROOT_PATH)) { list.addAll(items); } else { for (int i = 0, length = items.size(); i < length; i++) { ItemState item = items.get(i); if (item.isDescendantOf(rootPath)) { list.add(item); } } } return list; } /** * Gets items by identifier. * * @param itemIdentifier * @return */ public List<ItemState> getItemStates(String itemIdentifier) { List<ItemState> states = new ArrayList<ItemState>(); List<ItemState> currentStates = getAllStates(); for (int i = 0, length = currentStates.size(); i < length; i++) { ItemState state = currentStates.get(i); if (state.getData().getIdentifier().equals(itemIdentifier)) { states.add(state); } } return states; } /** * Collect last in ChangesLog order node (and direct childs) changes. * * @param rootData * - a item root of the changes scan * @return this item (!) and child items last modify states (i.e. updates, not adds or deletes) */ public Collection<ItemState> getLastModifyStates(NodeData rootData) { HashMap<String, ItemState> changes = new HashMap<String, ItemState>(); for (int i = 0; i < items.size(); i++) { ItemData item = items.get(i).getData(); if (item.getIdentifier().equals(rootData.getIdentifier())) { // the node if (items.get(i).isAdded()) { // if a new item - no modify changes can be return new ArrayList<ItemState>(); } if (!items.get(i).isDeleted()) { changes.put(item.getIdentifier(), items.get(i)); } } else if (item.getParentIdentifier().equals(rootData.getIdentifier())) { // childs changes.put(item.getIdentifier(), items.get(i)); } } return changes.values(); } /** * Search for an item state of item with given id and filter parameters. * * @param id * - item id * @param states * - filter only the given list states (ORed), or all if it's null * @param isPersisted * - filter only persisted/not persisted, or all if it's null * @return - filtered {@link ItemState} * @throws IllegalPathException */ public ItemState findItemState(String id, Boolean isPersisted, int... states) throws IllegalPathException { List<ItemState> allStates = getAllStates(); // search from the end for state for (int i = allStates.size() - 1; i >= 0; i--) { ItemState istate = allStates.get(i); boolean byState = false; if (states != null) { for (int state : states) { if (istate.getState() == state) { byState = true; break; } } } else { byState = true; } if (byState && (isPersisted != null ? istate.isPersisted() == isPersisted : true) && istate.getData().getIdentifier().equals(id)) { return istate; } } return null; } /** * This class is used as a key for index map. */ protected class IDStateBasedKey { /** * Item identifier. */ private final String identifier; /** * Item state. */ private final int state; /** * KeyUUIDState constructor. * * @param identifier * item identifier * @param state * item state */ public IDStateBasedKey(String identifier, int state) { this.identifier = identifier; this.state = state; } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + identifier.hashCode(); result = prime * result + state; return result; } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } IDStateBasedKey other = (IDStateBasedKey)obj; if (identifier == null) { if (other.identifier != null) { return false; } } else if (!identifier.equals(other.identifier)) { return false; } if (state != other.state) { return false; } return true; } } /** * This class is used as a key for index map. */ protected class ParentIDQPathBasedKey { /** * Item name. */ private final QPathEntry name; /** * Parent identifier. */ private final String parentIdentifier; private final ItemType itemType; /** * KeyParentUUIDQPath constructor. * * @param item * the item */ public ParentIDQPathBasedKey(ItemState item) { this.name = item.getData().getQPath().getEntries()[item.getData().getQPath().getEntries().length - 1]; this.parentIdentifier = item.getData().getParentIdentifier(); this.itemType = ItemType.getItemType(item.getData()); } /** * KeyParentUUIDQPath constructor. * * @param parentIdentifier * the parent identifier * @param name * item name */ ParentIDQPathBasedKey(String parentIdentifier, QPathEntry name, ItemType itemType) { this.name = name; this.parentIdentifier = parentIdentifier; this.itemType = itemType; } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + name.getName().hashCode(); result = prime * result + name.getNamespace().hashCode(); result = prime * result + name.getIndex(); result = prime * result + (parentIdentifier == null ? 0 : parentIdentifier.hashCode()); result = prime * result + itemType.ordinal(); return result; } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ParentIDQPathBasedKey other = (ParentIDQPathBasedKey)obj; if (name == null) { if (other.name != null) { return false; } } else if (!name.getName().equals(other.name.getName()) || !name.getNamespace().equals(other.name.getNamespace()) || name.getIndex() != other.name.getIndex()) { return false; } if (parentIdentifier == null) { if (other.parentIdentifier != null) { return false; } } else if (!parentIdentifier.equals(other.parentIdentifier)) { return false; } if (itemType == null) { if (other.itemType != null) { return false; } } else if (!itemType.equals(other.itemType)) { return false; } return true; } } // Need for Externalizable // ------------------ [ BEGIN ] ------------------ public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(eventType); byte[] buff = sessionId.getBytes(Constants.DEFAULT_ENCODING); out.writeInt(buff.length); out.write(buff); int listSize = items.size(); out.writeInt(listSize); for (int i = 0; i < listSize; i++) { out.writeObject(items.get(i)); } if (pairId != null) { out.writeInt(NOT_NULL_VALUE); buff = pairId.getBytes(Constants.DEFAULT_ENCODING); out.writeInt(buff.length); out.write(buff); } else { out.writeInt(NULL_VALUE); } } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { eventType = in.readInt(); byte[] buf = new byte[in.readInt()]; in.readFully(buf); sessionId = new String(buf, Constants.DEFAULT_ENCODING); int listSize = in.readInt(); for (int i = 0; i < listSize; i++) { add((ItemState)in.readObject()); } if (in.readInt() == NOT_NULL_VALUE) { buf = new byte[in.readInt()]; in.readFully(buf); pairId = new String(buf, Constants.DEFAULT_ENCODING); } } // ------------------ [ END ] ------------------ }