/*
* Copyright (C) 2009 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.services.jcr.impl.dataflow.persistent.infinispan;
import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.services.ispn.DistributedCacheManager;
import org.exoplatform.services.jcr.access.AccessControlList;
import org.exoplatform.services.jcr.config.CacheEntry;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.PersistedNodeData;
import org.exoplatform.services.jcr.dataflow.persistent.PersistedPropertyData;
import org.exoplatform.services.jcr.dataflow.persistent.WorkspaceStorageCache;
import org.exoplatform.services.jcr.dataflow.persistent.WorkspaceStorageCacheListener;
import org.exoplatform.services.jcr.datamodel.IllegalPathException;
import org.exoplatform.services.jcr.datamodel.InternalQName;
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.NullItemData;
import org.exoplatform.services.jcr.datamodel.NullNodeData;
import org.exoplatform.services.jcr.datamodel.NullPropertyData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.datamodel.ValueData;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.backup.BackupException;
import org.exoplatform.services.jcr.impl.backup.Backupable;
import org.exoplatform.services.jcr.impl.backup.DataRestore;
import org.exoplatform.services.jcr.impl.backup.rdbms.DataRestoreContext;
import org.exoplatform.services.jcr.impl.core.itemfilters.QPathEntryFilter;
import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil;
import org.exoplatform.services.jcr.impl.dataflow.persistent.SimplePersistedSize;
import org.exoplatform.services.jcr.infinispan.AbstractMapper;
import org.exoplatform.services.jcr.infinispan.CacheKey;
import org.exoplatform.services.jcr.infinispan.ISPNCacheFactory;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.transaction.ActionNonTxAware;
import org.exoplatform.services.transaction.TransactionService;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.context.Flag;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.MapReduceTask;
import org.infinispan.distexec.mapreduce.Reducer;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.picocontainer.Startable;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
/**
* Created by The eXo Platform SAS.<br>
*
* Cache based on Infinispan.<br>
*
* <ul>
* <li>cache transparent: or item cached or not, we should not generate "not found" Exceptions </li>
* </ul>
*
* This cache implementation stores items by UUID and parent UUID with QPathEntry.
* Except this cache stores list of children UUID.
*
* <br>
* Current state notes (subject of change):
* <ul>
* <li>cache implements WorkspaceStorageCache, without any stuff about references and locks</li>
* <li>transaction style implemented via batches, do with JTA (i.e. via exo's TransactionService + JBoss TM)</li>
* </ul>
*
* @author anatoliy.bazko@exoplatform.org Anatoliy Bazko
* @version $Id: ISPNCacheWorkspaceStorageCache.java 3514 2010-11-22 16:14:36Z nzamosenchuk $
*/
@SuppressWarnings("unchecked")
public class ISPNCacheWorkspaceStorageCache implements WorkspaceStorageCache, Backupable, Startable
{
private static final Log LOG = ExoLogger//NOSONAR
.getLogger("exo.jcr.component.core.ISPNCacheWorkspaceStorageCache");//NOSONAR
/**
* Name of the cache in case of the distributed cache
*/
private static final String CACHE_NAME = "JCRCache";
/**
* This id will be the unique identifier of the workspace in case the
* distributed mode is enabled as the cache will be then shared so we
* need this id to prevent mixing data of different workspace. In case
* the workspace is not distributed the value of this variable will be
* null to avoid consuming more memory for nothing
*/
protected final String ownerId;
private final boolean enabled;
protected final BufferedISPNCache cache;
private final GlobalOperationCaller caller;
/**
* The list of all the listeners
*/
private final List<WorkspaceStorageCacheListener> listeners =
new CopyOnWriteArrayList<WorkspaceStorageCacheListener>();
private final CacheActionNonTxAware<Void, Void> commitTransaction = new CacheActionNonTxAware<Void, Void>()
{
@Override
protected Void execute(Void arg)
{
cache.commitTransaction();
return null;
}
};
private final CacheActionNonTxAware<ItemData, String> getFromCacheById =
new CacheActionNonTxAware<ItemData, String>()
{
@Override
protected ItemData execute(String id)
{
return id == null ? null : (ItemData)cache.get(new CacheId(getOwnerId(), id));
}
};
private final CacheActionNonTxAware<ItemData, String> getFromBufferedCacheById =
new CacheActionNonTxAware<ItemData, String>()
{
@Override
protected ItemData execute(String id)
{
return id == null ? null : (ItemData)cache.getFromBuffer(new CacheId(getOwnerId(), id));
}
};
private final CacheActionNonTxAware<List<NodeData>, NodeData> getChildNodes =
new CacheActionNonTxAware<List<NodeData>, NodeData>()
{
@Override
protected List<NodeData> execute(NodeData parent)
{
// get list of children uuids
final Set<String> set = (Set<String>)cache.get(new CacheNodesId(getOwnerId(), parent.getIdentifier()));
if (set != null)
{
if (set instanceof FakeValueSet)
{
return null;
}
final List<NodeData> childs = new ArrayList<NodeData>();
for (String childId : set)
{
NodeData child = (NodeData)cache.get(new CacheId(getOwnerId(), childId));
if (child == null)
{
return null;
}
childs.add(child);
}
// order children by orderNumber, as HashSet returns children in other order
Collections.sort(childs, new NodesOrderComparator<NodeData>());
return childs;
}
else
{
return null;
}
}
};
private final CacheActionNonTxAware<ItemData, Object> getFromCacheByPath =
new CacheActionNonTxAware<ItemData, Object>()
{
@Override
protected ItemData execute(Object... args)
{
String parentIdentifier = (String)args[0];
QPathEntry name = (QPathEntry)args[1];
ItemType itemType = (ItemType)args[2];
String itemId = null;
if (itemType == ItemType.UNKNOWN)
{
// Try as node first.
itemId = (String)cache.get(new CacheQPath(getOwnerId(), parentIdentifier, name, ItemType.NODE));
if (itemId == null || itemId.equals(NullItemData.NULL_ID))
{
// node with such a name is not found or marked as not-exist, so check the properties
String propId =
(String)cache.get(new CacheQPath(getOwnerId(), parentIdentifier, name, ItemType.PROPERTY));
if (propId != null)
{
itemId = propId;
}
}
}
else if (itemType == ItemType.NODE)
{
itemId = (String)cache.get(new CacheQPath(getOwnerId(), parentIdentifier, name, ItemType.NODE));;
}
else
{
itemId = (String)cache.get(new CacheQPath(getOwnerId(), parentIdentifier, name, ItemType.PROPERTY));;
}
if (itemId != null)
{
if (itemId.equals(NullItemData.NULL_ID))
{
if (itemType == ItemType.UNKNOWN || itemType == ItemType.NODE)
{
return new NullNodeData();
}
else
{
return new NullPropertyData();
}
}
else
{
return get(itemId);
}
}
return null;
}
};
private final CacheActionNonTxAware<Integer, NodeData> getChildNodesCount =
new CacheActionNonTxAware<Integer, NodeData>()
{
@Override
protected Integer execute(NodeData parent)
{
Set<String> list = (Set<String>)cache.get(new CacheNodesId(getOwnerId(), parent.getIdentifier()));
return list != null ? list.size() : -1;
}
};
private final CacheActionNonTxAware<List<PropertyData>, Object> getChildProps =
new CacheActionNonTxAware<List<PropertyData>, Object>()
{
@Override
protected List<PropertyData> execute(Object... args)
{
String parentId = (String)args[0];
boolean withValue = (Boolean)args[1];
// get list of children uuids
final Set<String> set = (Set<String>)cache.get(new CachePropsId(getOwnerId(), parentId));
if (set != null)
{
final List<PropertyData> childs = new ArrayList<PropertyData>();
for (String childId : set)
{
PropertyData child = (PropertyData)cache.get(new CacheId(getOwnerId(), childId));
if (child == null)
{
return null;
}
if (withValue && child.getValues().size() <= 0)
{
return null;
}
childs.add(child);
}
return childs;
}
else
{
return null;
}
}
};
private final CacheActionNonTxAware<List<PropertyData>, String> getReferencedProperties =
new CacheActionNonTxAware<List<PropertyData>, String>()
{
@Override
protected List<PropertyData> execute(String identifier)
{
// get list of children uuids
final Set<String> set = (Set<String>)cache.get(new CacheRefsId(getOwnerId(), identifier));
if (set != null)
{
final List<PropertyData> props = new ArrayList<PropertyData>();
for (String childId : set)
{
PropertyData prop = (PropertyData)cache.get(new CacheId(getOwnerId(), childId));
if (prop == null || prop instanceof NullItemData)
{
return null;
}
// add property as many times as has referenced values
List<ValueData> lData = prop.getValues();
for (int i = 0, length = lData.size(); i < length; i++)
{
ValueData vdata = lData.get(i);
try
{
if (ValueDataUtil.getString(vdata).equals(identifier))
{
props.add(prop);
}
}
catch (IllegalStateException e)
{
// property was not added, force read from lower layer
return null;
}
catch (RepositoryException e)
{
// property was not added, force read from lower layer
return null;
}
}
}
return props;
}
else
{
return null;
}
}
};
private final CacheActionNonTxAware<Long, Void> getSize = new CacheActionNonTxAware<Long, Void>()
{
@Override
protected Long execute(Void arg)
{
return (long)caller.getCacheSize();
}
};
/**
* Node order comparator for getChildNodes().
*/
class NodesOrderComparator<N extends NodeData> implements Comparator<NodeData>
{
/**
* {@inheritDoc}
*/
public int compare(NodeData n1, NodeData n2)
{
return n1.getOrderNumber() - n2.getOrderNumber();
}
}
class ChildItemsIterator<T extends ItemData> implements Iterator<T>
{
private Iterator<String> childs;
private Iterator<Entry<CacheKey, Object>> entries;
private String parentId;
private Class<? extends ItemData> type;
private T next;
ChildItemsIterator(String parentId, Class<? extends ItemData> type, CacheKey key)
{
Set<String> set = (Set<String>)cache.get(key);
if (set != null)
{
this.childs = ((Set<String>)cache.get(key)).iterator();
}
else
{
this.entries = cache.entrySet().iterator();
this.parentId = parentId;
this.type = type;
}
fetchNext();
}
protected void fetchNext()
{
if (childs != null)
{
// Case where all the children nodes have been loaded
if (childs.hasNext())
{
// traverse to the first existing or the end of children
T n = null;
do
{
n = (T)cache.get(new CacheId(getOwnerId(), childs.next()));
}
while (n == null && childs.hasNext());
next = n;
}
else
{
next = null;
}
}
else
{
// Case where all the children nodes have not been loaded
if (entries.hasNext())
{
// traverse to the first children node or the end of the entry set
T n = null;
do
{
Entry<CacheKey, Object> entry = entries.next();
if (entry.getKey() instanceof CacheId && type.isInstance(entry.getValue()))
{
ItemData item = (ItemData)entry.getValue();
if (parentId.equals(item.getParentIdentifier()))
{
n = (T)item;
}
}
}
while (n == null && entries.hasNext());
next = n;
}
else
{
next = null;
}
}
}
public boolean hasNext()
{
return next != null;
}
public T next()
{
if (next == null)
{
throw new NoSuchElementException();
}
final T current = next;
fetchNext();
return current;
}
public void remove()
{
throw new IllegalArgumentException("Not implemented");
}
}
class ChildNodesIterator<N extends NodeData> extends ChildItemsIterator<N>
{
ChildNodesIterator(String parentId)
{
super(parentId, NodeData.class, new CacheNodesId(getOwnerId(), parentId));
}
@Override
public N next()
{
return super.next();
}
}
class ChildPropertiesIterator<P extends PropertyData> extends ChildItemsIterator<P>
{
ChildPropertiesIterator(String parentId)
{
super(parentId, PropertyData.class, new CachePropsId(getOwnerId(), parentId));
}
@Override
public P next()
{
return super.next();
}
}
/**
* Cache constructor.
*
* @param wsConfig WorkspaceEntry workspace configuration
* @param cfm The configuration manager
* @throws RepositoryException if error of initialization
* @throws RepositoryConfigurationException if error of configuration
*/
public ISPNCacheWorkspaceStorageCache(WorkspaceEntry wsConfig, ConfigurationManager cfm) throws RepositoryException,
RepositoryConfigurationException
{
this(null, wsConfig, cfm, null, null);
}
/**
* Cache constructor.
*
* @param ctx The {@link ExoContainerContext} that owns the current component
* @param wsConfig WorkspaceEntry workspace configuration
* @param cfm The configuration manager
* @param ts TransactionService external transaction service
* @throws RepositoryException if error of initialization
* @throws RepositoryConfigurationException if error of configuration
*/
public ISPNCacheWorkspaceStorageCache(ExoContainerContext ctx, WorkspaceEntry wsConfig, ConfigurationManager cfm,
TransactionService ts) throws RepositoryException, RepositoryConfigurationException
{
this(ctx, wsConfig, cfm, null, ts);
}
/**
* Cache constructor.
*
* @param ctx The {@link ExoContainerContext} that owns the current component
* @param wsConfig WorkspaceEntry workspace configuration
* @param cfm The configuration manager
* @param dcm The distributed cache manager
* @throws RepositoryException if error of initialization
* @throws RepositoryConfigurationException if error of configuration
*/
public ISPNCacheWorkspaceStorageCache(ExoContainerContext ctx, WorkspaceEntry wsConfig, ConfigurationManager cfm,
DistributedCacheManager dcm) throws RepositoryException, RepositoryConfigurationException
{
this(ctx, wsConfig, cfm, dcm, null);
}
/**
* Cache constructor.
*
* @param ctx The {@link ExoContainerContext} that owns the current component
* @param wsConfig WorkspaceEntry workspace configuration
* @param cfm The configuration manager
* @param dcm The distributed cache manager
* @param ts TransactionService external transaction service
* @throws RepositoryException if error of initialization
* @throws RepositoryConfigurationException if error of configuration
*/
public ISPNCacheWorkspaceStorageCache(ExoContainerContext ctx, WorkspaceEntry wsConfig, ConfigurationManager cfm,
DistributedCacheManager dcm, TransactionService ts) throws RepositoryException, RepositoryConfigurationException
{
if (wsConfig.getCache() == null)
{
throw new RepositoryConfigurationException("Cache configuration not found");
}
this.enabled = wsConfig.getCache().isEnabled();
// create cache using custom factory
ISPNCacheFactory<CacheKey, Object> factory =
new ISPNCacheFactory<CacheKey, Object>(cfm, ts == null ? null : ts.getTransactionManager());
// create parent Infinispan instance
CacheEntry cacheEntry = wsConfig.getCache();
boolean useDistributedCache = cacheEntry.getParameterBoolean("use-distributed-cache", false);
Cache<CacheKey, Object> parentCache;
if (useDistributedCache)
{
// We expect a distributed cache
if (dcm == null)
{
throw new IllegalArgumentException("The DistributedCacheManager has not been defined in the configuration,"
+ " please configure it at root container level if you want to use a distributed cache.");
}
parentCache = dcm.getCache(CACHE_NAME);
this.ownerId = ctx.getName();
if (LOG.isDebugEnabled())
{
LOG.debug("The distributed cache has been enabled for the workspace whose unique id is " + ownerId);
}
}
else
{
parentCache = factory.createCache("Data_" + wsConfig.getUniqueName(), cacheEntry);
Configuration config = parentCache.getCacheConfiguration();
if (config.clustering().cacheMode() == CacheMode.DIST_SYNC
|| config.clustering().cacheMode() == CacheMode.DIST_ASYNC)
{
throw new IllegalArgumentException("Cache configuration not allowed, if you want to use the distributed "
+ "cache please enable the parameter 'use-distributed-cache' and configure the DistributedCacheManager.");
}
this.ownerId = null;
}
Boolean allowLocalChanges =
useDistributedCache ? cacheEntry.getParameterBoolean("allow-local-changes", Boolean.TRUE) : Boolean.TRUE;
this.cache = new BufferedISPNCache(parentCache, allowLocalChanges);
if (useDistributedCache)
{
this.caller = new DistributedOperationCaller();
}
else
{
this.caller = new GlobalOperationCaller();
cache.addListener(new CacheEventListener());
}
this.cache.start();
}
private boolean isDistributedMode()
{
return ownerId != null;
}
private String getOwnerId()
{
return ownerId;
}
/**
* Return TransactionManager used by ISPN backing the JCR cache.
*
* @return TransactionManager
*/
public TransactionManager getTransactionManager()
{
return cache.getTransactionManager();
}
/**
* {@inheritDoc}
*/
public void put(ItemData item)
{
// There is different commit processing for NullNodeData and ordinary ItemData.
if (item instanceof NullItemData)
{
putNullItem((NullItemData)item);
return;
}
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
if (item.isNode())
{
putNode((NodeData)item, ModifyChildOption.NOT_MODIFY);
}
else
{
putProperty((PropertyData)item, ModifyChildOption.NOT_MODIFY);
}
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void remove(ItemData item)
{
removeItem(item);
}
/**
* {@inheritDoc}
*/
public void remove(String identifier, ItemData item)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
cache.remove(new CacheId(getOwnerId(), identifier), item);
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void onSaveItems(final ItemStateChangesLog itemStates)
{
// if something happen we will rollback changes
boolean rollback = true;
try
{
ItemState lastDelete = null;
cache.beginTransaction();
Set<String> idsToSkip = null;
List<ItemState> states = itemStates.getAllStates();
for (int i = 0, length = states.size(); i < length; i++)
{
ItemState state = states.get(i);
if (state.isAdded())
{
if (state.isPersisted())
{
putItem(state.getData());
}
}
else if (state.isUpdated())
{
if (state.isPersisted())
{
// There was a problem with removing a list of samename siblings in on transaction,
// so putItemInBufferedCache(..) and updateInBufferedCache(..) used instead put(..) and update (..) methods.
ItemData prevItem = putItemInBufferedCache(state.getData());
if (state.isNode() && (prevItem != null || state.getOldPath() != null))
{
// nodes reordered, if previous is null it's InvalidItemState case
idsToSkip =
updateInBuffer((NodeData)state.getData(),
prevItem != null ? prevItem.getQPath() : state.getOldPath(), idsToSkip);
if (i + 1 < length)
{
// We check if the next state is another update on a persisted node, because if so we can keep the skip list otherwise we can get
// rid of it
ItemState nextState = states.get(i + 1);
if (!nextState.isUpdated() || !nextState.isNode() || !nextState.isPersisted())
{
// No order before has been detected so we have no need to keep the list of ids
idsToSkip = null;
}
}
}
}
}
else if (state.isDeleted())
{
if (state.isPersisted())
{
removeItem(state.getData());
}
}
else if (state.isRenamed())
{
renameItem(state, lastDelete);
}
else if (state.isPathChanged())
{
updateTreePath(state.getOldPath(), state.getData().getQPath(), (Set<String>)null);
}
else if (state.isMixinChanged())
{
if (state.isPersisted())
{
// update subtree ACLs
updateMixin((NodeData)state.getData());
}
}
if (state.isDeleted())
{
lastDelete = state;
}
}
cache.commitTransaction();
rollback = false;
}
finally
{
if (rollback)
{
cache.rollbackTransaction();
}
}
}
/**
* {@inheritDoc}
*/
public void addChildNodesByPage(NodeData parent, List<NodeData> childs, int fromOrderNum)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
CacheNodesByPageId cacheId = new CacheNodesByPageId(getOwnerId(), parent.getIdentifier());
Map<Integer, Set<String>> pages = (Map<Integer, Set<String>>)cache.get(cacheId);
if (pages == null)
{
pages = new HashMap<Integer, Set<String>>();
}
Set<String> set = new HashSet<String>();
for (NodeData child : childs)
{
putNode(child, ModifyChildOption.NOT_MODIFY);
set.add(child.getIdentifier());
}
pages.put(fromOrderNum, set);
cache.put(cacheId, pages);
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void addChildNodes(NodeData parent, List<NodeData> childs)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
Set<Object> set = new HashSet<Object>();
for (NodeData child : childs)
{
putNode(child, ModifyChildOption.NOT_MODIFY);
set.add(child.getIdentifier());
}
cache.putIfAbsent(new CacheNodesId(getOwnerId(), parent.getIdentifier()), set);
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void addChildNodes(NodeData parent, QPathEntryFilter pattern, List<NodeData> childs)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
Set<String> set = new HashSet<String>();
for (NodeData child : childs)
{
putNode(child, ModifyChildOption.NOT_MODIFY);
set.add(child.getIdentifier());
}
CachePatternNodesId cacheId = new CachePatternNodesId(getOwnerId(), parent.getIdentifier());
Map<QPathEntryFilter, Set<String>> patterns = (Map<QPathEntryFilter, Set<String>>)cache.get(cacheId);
if (patterns == null)
{
patterns = new HashMap<QPathEntryFilter, Set<String>>();
}
patterns.put(pattern, set);
cache.put(cacheId, patterns);
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void addChildProperties(NodeData parent, List<PropertyData> childs)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
if (childs.size() > 0)
{
// add all new
Set<Object> set = new HashSet<Object>();
for (PropertyData child : childs)
{
putProperty(child, ModifyChildOption.NOT_MODIFY);
set.add(child.getIdentifier());
}
cache.putIfAbsent(new CachePropsId(getOwnerId(), parent.getIdentifier()), set);
}
else
{
LOG.warn("Empty properties list cached " + (parent != null ? parent.getQPath().getAsString() : parent));
}
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void addChildProperties(NodeData parent, QPathEntryFilter pattern, List<PropertyData> childs)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
if (childs.size() > 0)
{
// add all new
Set<String> set = new HashSet<String>();
for (PropertyData child : childs)
{
putProperty(child, ModifyChildOption.NOT_MODIFY);
set.add(child.getIdentifier());
}
CachePatternPropsId cacheId = new CachePatternPropsId(getOwnerId(), parent.getIdentifier());
Map<QPathEntryFilter, Set<String>> patterns = (Map<QPathEntryFilter, Set<String>>)cache.get(cacheId);
if (patterns == null)
{
patterns = new HashMap<QPathEntryFilter, Set<String>>();
}
patterns.put(pattern, set);
cache.put(cacheId, patterns);
}
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public void addChildPropertiesList(NodeData parent, List<PropertyData> childProperties)
{
}
/**
* {@inheritDoc}
*/
public ItemData get(String parentIdentifier, QPathEntry name, ItemType itemType)
{
return getFromCacheByPath.run(parentIdentifier, name, itemType);
}
/**
* {@inheritDoc}
*/
public ItemData get(String id)
{
return getFromCacheById.run(id);
}
/**
* {@inheritDoc}
*/
public List<NodeData> getChildNodes(final NodeData parent)
{
return getChildNodes.run(parent);
}
/**
* {@inheritDoc}
*/
public void addChildNodesCount(NodeData parent, int count)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
cache.putIfAbsent(new CacheNodesId(getOwnerId(), parent.getIdentifier()), new FakeValueSet(count));
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public List<NodeData> getChildNodesByPage(final NodeData parent, final int fromOrderNum)
{
// get list of children uuids
final Map<Integer, Set<String>> pages =
(Map<Integer, Set<String>>)cache.get(new CacheNodesByPageId(getOwnerId(), parent.getIdentifier()));
if (pages == null)
{
return null;
}
Set<String> set = pages.get(fromOrderNum);
if (set == null)
{
return null;
}
final List<NodeData> childs = new ArrayList<NodeData>();
for (String childId : set)
{
NodeData child = (NodeData)cache.get(new CacheId(getOwnerId(), childId));
if (child == null)
{
return null;
}
childs.add(child);
}
// order children by orderNumber, as HashSet returns children in other order
Collections.sort(childs, new NodesOrderComparator<NodeData>());
return childs;
}
/**
* {@inheritDoc}
*/
public List<NodeData> getChildNodes(final NodeData parent, final QPathEntryFilter pattern)
{
// get list of children uuids
final Map<QPathEntryFilter, Set<String>> patterns =
(Map<QPathEntryFilter, Set<String>>)cache.get(new CachePatternNodesId(getOwnerId(), parent.getIdentifier()));
if (patterns == null)
{
return null;
}
Set<String> set = patterns.get(pattern);
if (set == null)
{
return null;
}
final List<NodeData> childs = new ArrayList<NodeData>();
for (String childId : set)
{
NodeData child = (NodeData)cache.get(new CacheId(getOwnerId(), childId));
if (child == null)
{
return null;
}
childs.add(child);
}
// order children by orderNumber, as HashSet returns children in other order
Collections.sort(childs, new NodesOrderComparator<NodeData>());
return childs;
}
/**
* {@inheritDoc}
*/
public int getChildNodesCount(NodeData parent)
{
return getChildNodesCount.run(parent);
}
/**
* {@inheritDoc}
*/
public List<PropertyData> getChildProperties(NodeData parent)
{
return getChildProps(parent.getIdentifier(), true);
}
public List<PropertyData> getChildProperties(NodeData parent, QPathEntryFilter pattern)
{
// get list of children uuids
final Map<QPathEntryFilter, Set<String>> patterns =
(Map<QPathEntryFilter, Set<String>>)cache.get(new CachePatternPropsId(getOwnerId(), parent.getIdentifier()));
if (patterns == null)
{
return null;
}
Set<String> set = patterns.get(pattern);
if (set == null)
{
return null;
}
final List<PropertyData> childs = new ArrayList<PropertyData>();
for (String childId : set)
{
PropertyData child = (PropertyData)cache.get(new CacheId(getOwnerId(), childId));
if (child == null)
{
return null;
}
if (child.getValues().size() <= 0)
{
return null;
}
childs.add(child);
}
return childs;
}
/**
* {@inheritDoc}
*/
public List<PropertyData> listChildProperties(NodeData parent)
{
return getChildProps(parent.getIdentifier(), false);
}
/**
* Internal get child properties.
*
* @param parentId String
* @param withValue boolean, if true only "full" Propeties can be returned
* @return List of PropertyData
*/
protected List<PropertyData> getChildProps(String parentId, boolean withValue)
{
return getChildProps.run(parentId, withValue);
}
/**
* {@inheritDoc}
*/
public long getSize()
{
return getSize.run();
}
/**
* {@inheritDoc}
*/
public boolean isEnabled()
{
return enabled;
}
/**
* {@inheritDoc}
*/
public boolean isPatternSupported()
{
return true;
}
/**
* {@inheritDoc}
*/
public boolean isChildNodesByPageSupported()
{
return true;
}
/**
* Internal put Item.
*
* @param item ItemData, new data to put in the cache
* @return ItemData, previous data or null
*/
protected ItemData putItem(ItemData item)
{
if (item.isNode())
{
return putNode((NodeData)item, ModifyChildOption.MODIFY);
}
else
{
return putProperty((PropertyData)item, ModifyChildOption.MODIFY);
}
}
protected ItemData putItemInBufferedCache(ItemData item)
{
if (item.isNode())
{
return putNodeInBufferedCache((NodeData)item, ModifyChildOption.MODIFY);
}
else
{
return putProperty((PropertyData)item, ModifyChildOption.MODIFY);
}
}
/**
* Internal put Node.
*
* @param node, NodeData, new data to put in the cache
* @return NodeData, previous data or null
*/
protected ItemData putNode(NodeData node, ModifyChildOption modifyListsOfChild)
{
if (node.getParentIdentifier() != null)
{
if (modifyListsOfChild == ModifyChildOption.NOT_MODIFY)
{
cache.putIfAbsent(new CacheQPath(getOwnerId(), node.getParentIdentifier(), node.getQPath(), ItemType.NODE),
node.getIdentifier());
}
else
{
cache.put(new CacheQPath(getOwnerId(), node.getParentIdentifier(), node.getQPath(), ItemType.NODE),
node.getIdentifier());
}
// if MODIFY and List present OR FORCE_MODIFY, then write
if (modifyListsOfChild != ModifyChildOption.NOT_MODIFY)
{
cache.addToPatternList(new CachePatternNodesId(getOwnerId(), node.getParentIdentifier()), node);
cache.addToList(new CacheNodesId(getOwnerId(), node.getParentIdentifier()), node.getIdentifier(),
modifyListsOfChild == ModifyChildOption.FORCE_MODIFY);
cache.remove(new CacheNodesByPageId(getOwnerId(), node.getParentIdentifier()));
}
}
if (modifyListsOfChild == ModifyChildOption.NOT_MODIFY)
{
return (ItemData)cache.putIfAbsent(new CacheId(getOwnerId(), node.getIdentifier()), node);
}
else
{
return (ItemData)cache.put(new CacheId(getOwnerId(), node.getIdentifier()), node, true);
}
}
protected ItemData putNodeInBufferedCache(NodeData node, ModifyChildOption modifyListsOfChild)
{
if (node.getParentIdentifier() != null)
{
cache.put(new CacheQPath(getOwnerId(), node.getParentIdentifier(), node.getQPath(), ItemType.NODE),
node.getIdentifier());
// if MODIFY and List present OR FORCE_MODIFY, then write
if (modifyListsOfChild != ModifyChildOption.NOT_MODIFY)
{
cache.addToList(new CacheNodesId(getOwnerId(), node.getParentIdentifier()), node.getIdentifier(),
modifyListsOfChild == ModifyChildOption.FORCE_MODIFY);
}
}
// NullNodeData must never be returned inside internal cache operations.
ItemData itemData = (ItemData)cache.putInBuffer(new CacheId(getOwnerId(), node.getIdentifier()), node);
return (itemData instanceof NullItemData) ? null : itemData;
}
/**
* Internal put NullNode.
*
* @param item, NullItemData, new data to put in the cache
*/
protected void putNullItem(NullItemData item)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
if (!item.getIdentifier().equals(NullItemData.NULL_ID))
{
cache.putIfAbsent(new CacheId(getOwnerId(), item.getIdentifier()), item);
}
else if (item.getName() != null && item.getParentIdentifier() != null)
{
cache.putIfAbsent(
new CacheQPath(getOwnerId(), item.getParentIdentifier(), item.getName(), ItemType.getItemType(item)),
NullItemData.NULL_ID);
}
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* Internal put Property.
*
* @param prop, PropertyData, new data to put in the cache
* @param modifyListsOfChild
* @return PropertyData, previous data or null
*/
protected PropertyData putProperty(PropertyData prop, ModifyChildOption modifyListsOfChild)
{
// if MODIFY and List present OR FORCE_MODIFY, then write
if (modifyListsOfChild != ModifyChildOption.NOT_MODIFY)
{
cache.addToPatternList(new CachePatternPropsId(getOwnerId(), prop.getParentIdentifier()), prop);
cache.addToList(new CachePropsId(getOwnerId(), prop.getParentIdentifier()), prop.getIdentifier(),
modifyListsOfChild == ModifyChildOption.FORCE_MODIFY);
}
if (modifyListsOfChild == ModifyChildOption.NOT_MODIFY)
{
cache.putIfAbsent(
new CacheQPath(getOwnerId(), prop.getParentIdentifier(), prop.getQPath(), ItemType.PROPERTY),
prop.getIdentifier());
}
else
{
cache.put(new CacheQPath(getOwnerId(), prop.getParentIdentifier(), prop.getQPath(), ItemType.PROPERTY),
prop.getIdentifier());
}
// add referenced property
if (modifyListsOfChild != ModifyChildOption.NOT_MODIFY && prop.getType() == PropertyType.REFERENCE)
{
List<ValueData> lData = prop.getValues();
for (int i = 0, length = lData.size(); i < length; i++)
{
ValueData vdata = lData.get(i);
String nodeIdentifier = null;
try
{
nodeIdentifier = ValueDataUtil.getString(vdata);
}
catch (IllegalStateException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
catch (RepositoryException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
cache.addToList(new CacheRefsId(getOwnerId(), nodeIdentifier), prop.getIdentifier(),
modifyListsOfChild == ModifyChildOption.FORCE_MODIFY);
}
}
// NullItemData must never be returned inside internal cache operations.
ItemData returnedData;
if (modifyListsOfChild == ModifyChildOption.NOT_MODIFY)
{
returnedData = (ItemData)cache.putIfAbsent(new CacheId(getOwnerId(), prop.getIdentifier()), prop);
}
else
{
returnedData = (ItemData)cache.put(new CacheId(getOwnerId(), prop.getIdentifier()), prop, true);
}
return (returnedData instanceof NullItemData) ? null : (PropertyData) returnedData;
}
protected void removeItem(ItemData item)
{
cache.remove(new CacheId(getOwnerId(), item.getIdentifier()));
cache
.remove(new CacheQPath(getOwnerId(), item.getParentIdentifier(), item.getQPath(), ItemType.getItemType(item)));
if (item.isNode())
{
if (item.getParentIdentifier() != null)
{
cache.removeFromPatternList(new CachePatternNodesId(getOwnerId(), item.getParentIdentifier()), item);
cache.removeFromList(new CacheNodesId(getOwnerId(), item.getParentIdentifier()), item.getIdentifier());
cache.remove(new CacheNodesByPageId(getOwnerId(), item.getParentIdentifier()));
}
cache.remove(new CacheNodesId(getOwnerId(), item.getIdentifier()));
cache.remove(new CachePropsId(getOwnerId(), item.getIdentifier()));
cache.remove(new CacheNodesByPageId(getOwnerId(), item.getIdentifier()));
cache.remove(new CachePatternNodesId(getOwnerId(), item.getIdentifier()));
cache.remove(new CachePatternPropsId(getOwnerId(), item.getIdentifier()));
cache.remove(new CacheRefsId(getOwnerId(), item.getIdentifier()));
}
else
{
cache.removeFromPatternList(new CachePatternPropsId(getOwnerId(), item.getParentIdentifier()), item);
cache.removeFromList(new CachePropsId(getOwnerId(), item.getParentIdentifier()), item.getIdentifier());
}
}
/**
* Update Node's mixin and ACL.
*
* @param node NodeData
*/
protected void updateMixin(NodeData node)
{
NodeData prevData = (NodeData)cache.put(new CacheId(getOwnerId(), node.getIdentifier()), node, true);
// prevent update NullNodeData
if (!(prevData instanceof NullNodeData))
{
if (prevData != null)
{
// do update ACL if needed
if (prevData.getACL() == null || !prevData.getACL().equals(node.getACL()))
{
updateChildsACL(node.getIdentifier(), node.getACL());
}
}
else if (LOG.isDebugEnabled())
{
LOG.debug("Previous NodeData not found for mixin update " + node.getQPath().getAsString());
}
}
}
/**
* Update Node hierarchy in case of same-name siblings reorder.
* Assumes the new (updated) nodes already put in the cache. Previous name of updated nodes will be calculated
* and that node will be deleted (if has same id as the new node). Children paths will be updated to a new node path.
*
* @param node new node data
* @param prevPath old path
* @param idsToSkip set of ids to skip, this is needed to avoid modifying the path twice in case of an OrderBefore
* @return the ids to skip for the next operation
*/
protected Set<String> updateInBuffer(final NodeData node, final QPath prevPath, Set<String> idsToSkip)
{
// I expect that NullNodeData will never update existing NodeData.
CacheQPath prevKey = new CacheQPath(getOwnerId(), node.getParentIdentifier(), prevPath, ItemType.NODE);
if (node.getIdentifier().equals(cache.getFromBuffer(prevKey)))
{
cache.remove(prevKey);
}
// update childs paths if index changed
int nodeIndex = node.getQPath().getEntries()[node.getQPath().getEntries().length - 1].getIndex();
int prevNodeIndex = prevPath.getEntries()[prevPath.getEntries().length - 1].getIndex();
if (nodeIndex != prevNodeIndex)
{
// its a same name reordering
return updateTreePath(prevPath, node.getQPath(), idsToSkip);
}
return null;
}
/**
* Check all items in cache if it is a descendant of the previous root path, and if so update the path according
* the new root path.
*
* @param prevRootPath previous root path
* @param newRootPath new root path
* @param idsToSkip set of ids to skip, this is needed to avoid modifying the path twice in case of an OrderBefore
* @return the ids to skip for the next operation
*/
protected Set<String> updateTreePath(QPath prevRootPath, QPath newRootPath, Set<String> idsToSkip)
{
return caller.updateTreePath(prevRootPath, newRootPath, idsToSkip);
}
/**
* Apply rename operation on cache. Parent node will be re-added into the cache since
* parent or name might changing. For other children only item data will be replaced.
*/
protected void renameItem(final ItemState state, final ItemState lastDelete)
{
ItemData data = state.getData();
ItemData prevData = getFromBufferedCacheById.run(data.getIdentifier());
if (data.isNode())
{
if (state.isPersisted())
{
// it is state where name can be changed by rename operation, so re-add node
removeItem(lastDelete.getData());
putItem(state.getData());
}
else
{
// update item data with new name only
cache.put(new CacheId(getOwnerId(), data.getIdentifier()), data, false);
}
}
else
{
PropertyData prop = (PropertyData)data;
if (prevData != null && !(prevData instanceof NullItemData))
{
PropertyData newProp =
new PersistedPropertyData(prop.getIdentifier(), prop.getQPath(), prop.getParentIdentifier(),
prop.getPersistedVersion(), prop.getType(), prop.isMultiValued(),
((PropertyData)prevData).getValues(), new SimplePersistedSize(
((PersistedPropertyData)prevData).getPersistedSize()));
// update item data with new name and old values only
cache.put(new CacheId(getOwnerId(), newProp.getIdentifier()), newProp, false);
}
else
{
// remove item to avoid inconsistency in cluster mode since we have not old values
cache.remove(new CacheId(getOwnerId(), data.getIdentifier()));
}
}
}
/**
* Update child Nodes ACLs.
*
* @param parentId String - root node id of JCR subtree.
* @param acl AccessControlList
*/
protected void updateChildsACL(String parentId, AccessControlList acl)
{
caller.updateChildsACL(parentId, acl);
}
public void beginTransaction()
{
cache.beginTransaction();
}
public void commitTransaction()
{
cache.commitTransaction();
}
public void rollbackTransaction()
{
cache.rollbackTransaction();
}
/**
* {@inheritDoc}
*/
public boolean isTXAware()
{
return true;
}
/**
* <li>NOT_MODIFY - node(property) is not added to the parent's list
* (no persistent changes performed, cache used as cache)</li>
* <li>MODIFY - node(property) is added to the parent's list if parent in the cache
* (new item is added to persistent, add to list if it is present)</li>
* <li>FORCE_MODIFY - node(property) is added to the parent's list anyway (when list is read from DB, forcing write)</li>
*/
private enum ModifyChildOption {
NOT_MODIFY, MODIFY, FORCE_MODIFY
}
/**
* Allows to commit the cache changes in a dedicated XA Tx in order to avoid potential
* deadlocks
*/
private void dedicatedTxCommit()
{
commitTransaction.run();
}
/**
* {@inheritDoc}
*/
public void addReferencedProperties(String identifier, List<PropertyData> refProperties)
{
boolean inTransaction = cache.isTransactionActive();
try
{
if (!inTransaction)
{
cache.beginTransaction();
}
cache.setLocal(true);
Set<Object> set = new HashSet<Object>();
for (PropertyData prop : refProperties)
{
putProperty(prop, ModifyChildOption.NOT_MODIFY);
set.add(prop.getIdentifier());
}
cache.putIfAbsent(new CacheRefsId(getOwnerId(), identifier), set);
}
finally
{
cache.setLocal(false);
if (!inTransaction)
{
dedicatedTxCommit();
}
}
}
/**
* {@inheritDoc}
*/
public List<PropertyData> getReferencedProperties(String identifier)
{
return getReferencedProperties.run(identifier);
}
/**
* {@inheritDoc}
*/
public void backup(File storageDir) throws BackupException
{
}
/**
* {@inheritDoc}
*/
@Managed
@ManagedDescription("Remove all the existing items from the cache")
public void clean() throws BackupException
{
LOG.info("Start to clean all the existing items from ISPN cache");
if (cache.getStatus() == ComponentStatus.RUNNING)
{
caller.clearCache();
}
}
/**
* {@inheritDoc}
*/
public DataRestore getDataRestorer(DataRestoreContext context) throws BackupException
{
return new DataRestore()
{
/**
* {@inheritDoc}
*/
public void clean() throws BackupException
{
LOG.info("Start to clean all the existing items from ISPN cache");
caller.clearCache();
}
/**
* {@inheritDoc}
*/
public void restore() throws BackupException
{
}
/**
* {@inheritDoc}
*/
public void commit() throws BackupException
{
}
/**
* {@inheritDoc}
*/
public void rollback() throws BackupException
{
}
/**
* {@inheritDoc}
*/
public void close() throws BackupException
{
}
};
}
/**
* {@inheritDoc}
*/
public void addListener(WorkspaceStorageCacheListener listener)
{
if (isDistributedMode())
{
throw new UnsupportedOperationException("The cache listeners are not supported by the "
+ "ISPNCacheWorkspaceStorageCache in case of the distributed mode");
}
listeners.add(listener);
}
/**
* {@inheritDoc}
*/
public void removeListener(WorkspaceStorageCacheListener listener)
{
if (isDistributedMode())
{
throw new UnsupportedOperationException("The cache listeners are not supported by the "
+ "ISPNCacheWorkspaceStorageCache in case of the distributed mode");
}
listeners.remove(listener);
}
/**
* Called when a cache entry corresponding to the given node has item updated
* @param data the item corresponding to the updated cache entry
*/
private void onCacheEntryUpdated(ItemData data)
{
if (data == null || data instanceof NullItemData)
{
return;
}
for (WorkspaceStorageCacheListener listener : listeners)
{
try
{
listener.onCacheEntryUpdated(data);
}
catch (RuntimeException e) //NOSONAR
{
LOG.warn("The method onCacheEntryUpdated fails for the listener " + listener.getClass(), e);
}
}
}
private static boolean updateTreePath(Cache<CacheKey, Object> cache, String ownerId, ItemData data, QPath prevRootPath,
QPath newRootPath)
{
if (data == null)
{
return false;
}
// check is this descendant of prevRootPath
QPath nodeQPath = data.getQPath();
if (nodeQPath != null && nodeQPath.isDescendantOf(prevRootPath))
{
//make relative path
QPathEntry[] relativePath = null;
try
{
relativePath = nodeQPath.getRelPath(nodeQPath.getDepth() - prevRootPath.getDepth());
}
catch (IllegalPathException e)
{
// Do nothing. Never happens.
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
if (relativePath == null)
{
LOG.error("Could not get the relative path of the node " + nodeQPath + " with "
+ (nodeQPath.getDepth() - prevRootPath.getDepth()) + " as relative degree");
return false;
}
// make new path - no matter node or property
QPath newPath = QPath.makeChildPath(newRootPath, relativePath);
if (data.isNode())
{
// update node
NodeData prevNode = (NodeData)data;
PersistedNodeData newNode =
new PersistedNodeData(prevNode.getIdentifier(), newPath, prevNode.getParentIdentifier(),
prevNode.getPersistedVersion(), prevNode.getOrderNumber(), prevNode.getPrimaryTypeName(),
prevNode.getMixinTypeNames(), prevNode.getACL());
// update this node
cache.put(new CacheId(ownerId, newNode.getIdentifier()), newNode);
}
else
{
//update property
PropertyData prevProp = (PropertyData)data;
PersistedPropertyData newProp =
new PersistedPropertyData(prevProp.getIdentifier(), newPath, prevProp.getParentIdentifier(),
prevProp.getPersistedVersion(), prevProp.getType(), prevProp.isMultiValued(), prevProp.getValues(),
new SimplePersistedSize(((PersistedPropertyData)prevProp).getPersistedSize()));
// update this property
cache.put(new CacheId(ownerId, newProp.getIdentifier()), newProp);
}
return true;
}
return false;
}
/**
* Actions that are not supposed to be called within a transaction
*
* Created by The eXo Platform SAS
* Author : Nicolas Filotto
* nicolas.filotto@exoplatform.com
* 21 janv. 2010
*/
protected abstract class CacheActionNonTxAware<R, A> extends ActionNonTxAware<R, A, RuntimeException>
{
/**
* {@inheritDoc}
*/
protected TransactionManager getTransactionManager()
{
return ISPNCacheWorkspaceStorageCache.this.getTransactionManager();
}
}
@SuppressWarnings("rawtypes")
@Listener
public class CacheEventListener
{
@CacheEntryModified
public void cacheEntryModified(CacheEntryModifiedEvent evt)
{
if (!evt.isPre() && evt.getKey() instanceof CacheId)
{
final ItemData value = (ItemData)evt.getValue();
onCacheEntryUpdated(value);
}
}
}
/**
* This class defines all the methods that could change between the replicated and the distributed mode.
* By default it implements the methods for the local and replicated mode.
*
*/
private class GlobalOperationCaller
{
protected int getCacheSize()
{
return cache.size();
}
protected void clearCache()
{
cache.clear();
}
/**
* Update child Nodes ACLs.
*
* @param parentId String - root node id of JCR subtree.
* @param acl AccessControlList
*/
protected void updateChildsACL(String parentId, AccessControlList acl)
{
loop : for (Iterator<NodeData> iter = new ChildNodesIterator<NodeData>(parentId); iter.hasNext();)
{
NodeData prevNode = iter.next();
// is ACL changes on this node (i.e. ACL inheritance brokes)
boolean hasExoPrivilegeable = false;
boolean hasExoOwneable = false;
for (InternalQName mixin : prevNode.getMixinTypeNames())
{
if (mixin.equals(Constants.EXO_PRIVILEGEABLE))
{
hasExoPrivilegeable = true;
if (hasExoOwneable)
{
continue loop;
}
}
else if (mixin.equals(Constants.EXO_OWNEABLE))
{
hasExoOwneable = true;
if (hasExoPrivilegeable)
{
continue loop;
}
}
}
AccessControlList newAcl = null;
if (hasExoOwneable)
{
newAcl = new AccessControlList(prevNode.getACL().getOwner(), acl.getPermissionEntries());
}
else if (hasExoPrivilegeable)
{
newAcl = new AccessControlList(acl.getOwner(), prevNode.getACL().getPermissionEntries());
}
if (newAcl != null)
{
if (newAcl.equals(prevNode.getACL()))
{
// No need to keep traversing the cache since the acl is the same
continue loop;
}
acl = newAcl;
}
// recreate with new path for child Nodes only
PersistedNodeData newNode =
new PersistedNodeData(prevNode.getIdentifier(), prevNode.getQPath(), prevNode.getParentIdentifier(),
prevNode.getPersistedVersion(), prevNode.getOrderNumber(), prevNode.getPrimaryTypeName(),
prevNode.getMixinTypeNames(), acl);
// update this node
cache.put(new CacheId(getOwnerId(), newNode.getIdentifier()), newNode);
// update childs recursive
updateChildsACL(newNode.getIdentifier(), acl);
}
}
/**
* Check all items in cache if it is a descendant of the previous root path, and if so update the path according
* the new root path.
*
* @param prevRootPath previous root path
* @param newRootPath new root path
* @param idsToSkip set of ids to skip, this is needed to avoid modifying the path twice in case of an OrderBefore
* @return the ids to skip for the next operation
*/
protected Set<String> updateTreePath(QPath prevRootPath, QPath newRootPath, Set<String> idsToSkip)
{
Set<String> result = new HashSet<String>();
Map<CacheKey, Object> changes = cache.getLastChanges();
for (CacheKey key : changes.keySet())
{
if (key instanceof CacheId)
{
ItemData data = (ItemData)changes.get(key);
if (data != null
&& (idsToSkip == null || !idsToSkip.contains(data.getIdentifier()))
&& ISPNCacheWorkspaceStorageCache
.updateTreePath(cache, getOwnerId(), data, prevRootPath, newRootPath))
{
result.add(data.getIdentifier());
}
}
}
// check all ITEMS in cache
for (CacheKey key : cache.keySet())
{
if (key instanceof CacheId && !changes.containsKey(key))
{
ItemData data = (ItemData)cache.get(key);
if (ISPNCacheWorkspaceStorageCache.updateTreePath(cache, getOwnerId(), data, prevRootPath, newRootPath))
{
result.add(data.getIdentifier());
}
}
}
return result;
}
}
/**
* This class implements all the global operations for the distributed mode
*
*/
private class DistributedOperationCaller extends GlobalOperationCaller
{
/**
* {@inheritDoc}
*/
@Override
protected int getCacheSize()
{
Map<String, Integer> map = SecurityHelper.doPrivilegedAction(new PrivilegedAction<Map<String, Integer>>()
{
public Map<String, Integer> run()
{
MapReduceTask<CacheKey, Object, String, Integer> task =
new MapReduceTask<CacheKey, Object, String, Integer>(cache);
task.mappedWith(new GetSizeMapper(getOwnerId())).reducedWith(new GetSizeReducer<String>());
return task.execute();
}
});
int sum = 0;
for (Integer i : map.values())
{
sum += i;
}
return sum;
}
/**
* {@inheritDoc}
*/
@Override
protected void clearCache()
{
SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
{
public Void run()
{
MapReduceTask<CacheKey, Object, Void, Void> task =
new MapReduceTask<CacheKey, Object, Void, Void>(cache);
task.mappedWith(new ClearCacheMapper(getOwnerId())).reducedWith(new IdentityReducer());
task.execute();
return null;
}
});
}
/**
* {@inheritDoc}
*/
@Override
protected Set<String> updateTreePath(final QPath prevRootPath, final QPath newRootPath, Set<String> idsToSkip)
{
Set<String> result = new HashSet<String>();
final TransactionManager tm = getTransactionManager();
if (tm != null)
{
try
{
// Add the action out of the current transaction to avoid deadlocks
tm.getTransaction().registerSynchronization(new Synchronization()
{
public void beforeCompletion()
{
}
public void afterCompletion(int status)
{
if (status == Status.STATUS_COMMITTED)
{
try
{
// Since the tx is successfully committed we can call components non tx aware
// The listeners will need to be executed outside the current tx so we suspend
// the current tx we can face enlistment issues on product like ISPN
tm.suspend();
_updateTreePath(prevRootPath, newRootPath);
}
catch (SystemException e)
{
LOG.warn("Cannot suspend the transaction", e);
}
}
}
});
return result;
}
catch (Exception e) //NOSONAR
{
if (LOG.isDebugEnabled())
{
LOG.debug("Cannot register the synchronization to the current transaction in order to update"
+ " the path out of the transaction", e);
}
}
}
_updateTreePath(prevRootPath, newRootPath);
return result;
}
private void _updateTreePath(final QPath prevRootPath, final QPath newRootPath)
{
SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
{
public Void run()
{
MapReduceTask<CacheKey, Object, Void, Void> task =
new MapReduceTask<CacheKey, Object, Void, Void>(cache);
task.mappedWith(new UpdateTreePathMapper(getOwnerId(), prevRootPath, newRootPath)).reducedWith(
new IdentityReducer());
task.execute();
return null;
}
});
}
/**
* {@inheritDoc}
*/
@Override
protected void updateChildsACL(String parentId, final AccessControlList acl)
{
ItemData parentItem = get(parentId);
if (!(parentItem instanceof NodeData))
{
return;
}
final QPath parentPath = ((NodeData)parentItem).getQPath();
SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
{
public Void run()
{
MapReduceTask<CacheKey, Object, Void, Void> task =
new MapReduceTask<CacheKey, Object, Void, Void>(cache);
task.mappedWith(new UpdateChildsACLMapper(getOwnerId(), parentPath, acl)).reducedWith(
new IdentityReducer());
task.execute();
return null;
}
});
}
}
public static class GetSizeMapper extends AbstractMapper<String, Integer>
{
public GetSizeMapper()
{
}
public GetSizeMapper(String ownerId)
{
super(ownerId);
}
/**
* {@inheritDoc}
*/
@Override
protected void _map(CacheKey key, Object value, Collector<String, Integer> collector)
{
collector.emit("total", Integer.valueOf(1));
}
}
public static class GetSizeReducer<K> implements Reducer<K, Integer>
{
/**
* The serial version UID
*/
private static final long serialVersionUID = 7877781449514234007L;
/**
* @see org.infinispan.distexec.mapreduce.Reducer#reduce(java.lang.Object, java.util.Iterator)
*/
public Integer reduce(K reducedKey, Iterator<Integer> iter)
{
int sum = 0;
while (iter.hasNext())
{
Integer i = iter.next();
sum += i;
}
return sum;
}
}
public static class ClearCacheMapper extends AbstractMapper<Void, Void>
{
public ClearCacheMapper()
{
}
public ClearCacheMapper(String ownerId)
{
super(ownerId);
}
/**
* {@inheritDoc}
*/
@Override
protected void _map(CacheKey key, Object value, Collector<Void, Void> collector)
{
ExoContainer container = ExoContainerContext.getTopContainer();
if (container == null)
{
LOG.error("The top container could not be found");
return;
}
DistributedCacheManager dcm =
(DistributedCacheManager)container.getComponentInstanceOfType(DistributedCacheManager.class);
if (dcm == null)
{
LOG.error("The DistributedCacheManager could not be found at top container level, please configure it.");
return;
}
Cache<CacheKey, Object> cache = dcm.getCache(CACHE_NAME);
cache.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP, Flag.FAIL_SILENTLY).remove(key);
}
}
public static class IdentityReducer implements Reducer<Void, Void>
{
/**
* The serial version UID
*/
private static final long serialVersionUID = -6193360351201912040L;
/**
* @see org.infinispan.distexec.mapreduce.Reducer#reduce(java.lang.Object, java.util.Iterator)
*/
public Void reduce(Void reducedKey, Iterator<Void> iter)
{
return null;
}
}
public static class UpdateTreePathMapper extends AbstractMapper<Void, Void>
{
private QPath prevRootPath, newRootPath;
public UpdateTreePathMapper()
{
}
public UpdateTreePathMapper(String ownerId, QPath prevRootPath, QPath newRootPath)
{
super(ownerId);
this.prevRootPath = prevRootPath;
this.newRootPath = newRootPath;
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isValid(CacheKey key)
{
return super.isValid(key) && key instanceof CacheId;
}
/**
* {@inheritDoc}
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
super.writeExternal(out);
byte[] buf = prevRootPath.getAsString().getBytes(Constants.DEFAULT_ENCODING);
out.writeInt(buf.length);
out.write(buf);
buf = newRootPath.getAsString().getBytes(Constants.DEFAULT_ENCODING);
out.writeInt(buf.length);
out.write(buf);
}
/**
* {@inheritDoc}
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
{
super.readExternal(in);
byte[] buf;
try
{
buf = new byte[in.readInt()];
in.readFully(buf);
String sQPath = new String(buf, Constants.DEFAULT_ENCODING);
prevRootPath = QPath.parse(sQPath);
buf = new byte[in.readInt()];
in.readFully(buf);
sQPath = new String(buf, Constants.DEFAULT_ENCODING);
newRootPath = QPath.parse(sQPath);
}
catch (IllegalPathException e)
{
throw new IOException("Deserialization error. ", e);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void _map(CacheKey key, Object value, Collector<Void, Void> collector)
{
ExoContainer container = ExoContainerContext.getTopContainer();
if (container == null)
{
LOG.error("The top container could not be found");
return;
}
DistributedCacheManager dcm =
(DistributedCacheManager)container.getComponentInstanceOfType(DistributedCacheManager.class);
if (dcm == null)
{
LOG.error("The DistributedCacheManager could not be found at top container level, please configure it.");
return;
}
Cache<CacheKey, Object> cache = dcm.getCache(CACHE_NAME);
ISPNCacheWorkspaceStorageCache.updateTreePath(cache.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP),
ownerId, (ItemData)value, prevRootPath, newRootPath);
}
}
public static class UpdateChildsACLMapper extends AbstractMapper<Void, Void>
{
private QPath parentPath;
private AccessControlList acl;
public UpdateChildsACLMapper()
{
}
public UpdateChildsACLMapper(String ownerId, QPath parentPath, AccessControlList acl)
{
super(ownerId);
this.parentPath = parentPath;
this.acl = acl;
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isValid(CacheKey key)
{
return super.isValid(key) && key instanceof CacheId;
}
/**
* {@inheritDoc}
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
super.writeExternal(out);
byte[] buf = parentPath.getAsString().getBytes(Constants.DEFAULT_ENCODING);
out.writeInt(buf.length);
out.write(buf);
out.writeBoolean(acl != null);
if (acl != null)
{
acl.writeExternal(out);
}
}
/**
* {@inheritDoc}
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
{
super.readExternal(in);
byte[] buf;
try
{
buf = new byte[in.readInt()];
in.readFully(buf);
String sQPath = new String(buf, Constants.DEFAULT_ENCODING);
parentPath = QPath.parse(sQPath);
}
catch (IllegalPathException e)
{
throw new IOException("Deserialization error. ", e);
}
if (in.readBoolean())
{
this.acl = new AccessControlList();
acl.readExternal(in);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void _map(CacheKey key, Object value, Collector<Void, Void> collector)
{
if (!(value instanceof NodeData))
{
return;
}
NodeData prevNode = (NodeData)value;
// check is this descendant of parentPath
QPath nodeQPath = prevNode.getQPath();
if (nodeQPath == null || !nodeQPath.isDescendantOf(parentPath))
{
return;
}
// is ACL changes on this node (i.e. ACL inheritance brokes)
boolean hasExoPrivilegeable = false;
boolean hasExoOwneable = false;
for (InternalQName mixin : prevNode.getMixinTypeNames())
{
if (mixin.equals(Constants.EXO_PRIVILEGEABLE))
{
hasExoPrivilegeable = true;
if (hasExoOwneable)
{
return;
}
}
else if (mixin.equals(Constants.EXO_OWNEABLE))
{
hasExoOwneable = true;
if (hasExoPrivilegeable)
{
return;
}
}
}
ExoContainer container = ExoContainerContext.getTopContainer();
if (container == null)
{
LOG.error("The top container could not be found");
return;
}
DistributedCacheManager dcm =
(DistributedCacheManager)container.getComponentInstanceOfType(DistributedCacheManager.class);
if (dcm == null)
{
LOG.error("The DistributedCacheManager could not be found at top container level, please configure it.");
return;
}
Cache<CacheKey, Object> cache = dcm.getCache(CACHE_NAME);
// we force the reloading
cache.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP, Flag.FAIL_SILENTLY).remove(key);
}
}
/**
* {@inheritDoc}
*/
public void start()
{
}
/**
* {@inheritDoc}
*/
public void stop()
{
cache.stop();
}
public static class FakeValueSet extends HashSet<String>
{
/**
* Serial Version UID
*/
private static final long serialVersionUID = 6163005084471981227L;
public FakeValueSet() {}
public FakeValueSet(int size)
{
for (int i = 0; i < size; i++)
{
// We put only fake ids to store the size and to force a reloading in case getChildNodes
// is called
add(Integer.toString(i));
}
}
}
}