/*
* Copyright (C) 2003-2010 eXo Platform SAS.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see<http://www.gnu.org/licenses/>.
*/
package org.exoplatform.services.jcr.impl.core.lock.cacheable;
import org.exoplatform.commons.utils.PrivilegedFileHelper;
import org.exoplatform.commons.utils.PrivilegedSystemHelper;
import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.dataflow.ChangesLogIterator;
import org.exoplatform.services.jcr.dataflow.CompositeChangesLog;
import org.exoplatform.services.jcr.dataflow.DataManager;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLogImpl;
import org.exoplatform.services.jcr.dataflow.TransactionChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
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.QPathEntry;
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.DummyDataRestore;
import org.exoplatform.services.jcr.impl.backup.rdbms.DBBackup;
import org.exoplatform.services.jcr.impl.backup.rdbms.DataRestoreContext;
import org.exoplatform.services.jcr.impl.core.SessionDataManager;
import org.exoplatform.services.jcr.impl.core.lock.LockRemover;
import org.exoplatform.services.jcr.impl.core.lock.LockRemoverHolder;
import org.exoplatform.services.jcr.impl.core.lock.LockTableHandler;
import org.exoplatform.services.jcr.impl.core.lock.SessionLockManager;
import org.exoplatform.services.jcr.impl.dataflow.TransientItemData;
import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData;
import org.exoplatform.services.jcr.impl.dataflow.persistent.WorkspacePersistentDataManager;
import org.exoplatform.services.jcr.impl.storage.JCRInvalidItemStateException;
import org.exoplatform.services.jcr.observation.ExtendedEvent;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.security.IdentityConstants;
import org.exoplatform.services.transaction.ActionNonTxAware;
import org.picocontainer.Startable;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;
/**
* Created by The eXo Platform SAS.
*
* <br>Date:
*
* @author <a href="karpenko.sergiy@gmail.com">Karpenko Sergiy</a>
* @version $Id: AbstractCacheableLockManagerImpl.java 2806 2010-07-21 08:00:15Z tolusha $
*/
public abstract class AbstractCacheableLockManager implements CacheableLockManager, ItemsPersistenceListener,
Startable, Backupable
{
/**
* The name to property time out.
*/
public static final String TIME_OUT = "time-out";
/**
* Default lock time out. 30min
*/
public static final long DEFAULT_LOCK_TIMEOUT = 1000 * 60 * 30;
/**
* Data manager.
*/
protected final DataManager dataManager;
/**
* Run time lock time out.
*/
protected long lockTimeOut;
/**
* Lock remover thread.
*/
protected LockRemover lockRemover;
/**
* Workspace configuration;
*/
protected final WorkspaceEntry config;
/**
* SessionLockManagers that uses this LockManager.
*/
protected Map<String, CacheableSessionLockManager> sessionLockManagers;
/**
* The current Transaction Manager
*/
protected TransactionManager tm;
/**
* Store datasource to have ability to get from context without recreation.
*/
protected DataSource dataSource;
public static final String LOCKS_FORCE_REMOVE = "org.exoplatform.jcr.locks.force.remove";
/**
* Logger
*/
protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.AbstractCacheableLockManager");
protected LockActionNonTxAware<Integer, Object> getNumLocks;
protected LockActionNonTxAware<Boolean, Object> hasLocks;
protected LockActionNonTxAware<Boolean, String> isLockLive;
protected LockActionNonTxAware<Object, LockData> refresh;
protected LockActionNonTxAware<Boolean, String> lockExist;
protected LockActionNonTxAware<LockData, String> getLockDataById;
protected LockActionNonTxAware<List<LockData>, Object> getLockList;
/**
* Constructor.
*
* @param dataManager - workspace persistent data manager
* @param config - workspace entry
* @param transactionManager
* the transaction manager
* @throws RepositoryConfigurationException
*/
public AbstractCacheableLockManager(WorkspacePersistentDataManager dataManager, WorkspaceEntry config,
TransactionManager transactionManager, LockRemoverHolder lockRemoverHolder)
throws RepositoryConfigurationException
{
if (config.getLockManager() != null)
{
if (config.getLockManager().hasParameters()
&& config.getLockManager().getParameterValue(TIME_OUT, null) != null)
{
long timeOut = config.getLockManager().getParameterTime(TIME_OUT);
lockTimeOut = timeOut > 0 ? timeOut : DEFAULT_LOCK_TIMEOUT;
}
}
else
{
lockTimeOut = DEFAULT_LOCK_TIMEOUT;
}
this.config = config;
this.dataManager = dataManager;
this.sessionLockManagers = new ConcurrentHashMap<String, CacheableSessionLockManager>();
this.tm = transactionManager;
this.lockRemover = lockRemoverHolder.getLockRemover(this);
dataManager.addItemPersistenceListener(this);
}
/**
* Returns the number of active locks.
*/
@Managed
@ManagedDescription("The number of active locks")
public int getNumLocks()
{
try
{
return getNumLocks.run();
}
catch (LockException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
return -1;
}
/**
* Indicates if some locks have already been created.
*/
protected boolean hasLocks()
{
try
{
return hasLocks.run();
}
catch (LockException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
return true;
}
/**
* Check is LockManager contains lock. No matter it is in pending or persistent state.
*/
public boolean isLockLive(String nodeId) throws LockException
{
try
{
return isLockLive.run(nodeId);
}
catch (LockException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
return false;
}
/**
* Refreshed lock data in cache
*/
public void refreshLockData(LockData newLockData) throws LockException
{
refresh.run(newLockData);
}
/**
* Check is LockManager contains lock.
*/
public boolean lockExist(String nodeId)
{
try
{
return lockExist.run(nodeId);
}
catch (LockException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
return false;
}
/**
* Returns lock data by node identifier.
*/
protected LockData getLockDataById(String nodeId)
{
try
{
return getLockDataById.run(nodeId);
}
catch (LockException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
return null;
}
/**
* Returns all locks.
*/
protected synchronized List<LockData> getLockList()
{
try
{
return getLockList.run();
}
catch (LockException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
return null;
}
@Managed
@ManagedDescription("Remove the expired locks")
public void cleanExpiredLocks()
{
removeExpired();
}
public long getDefaultLockTimeOut()
{
return lockTimeOut;
}
/**
* Return new instance of session lock manager.
*/
public SessionLockManager getSessionLockManager(String sessionId, SessionDataManager transientManager)
{
CacheableSessionLockManager sessionManager = new CacheableSessionLockManager(sessionId, this, transientManager);
sessionLockManagers.put(sessionId, sessionManager);
return sessionManager;
}
/**
* {@inheritDoc}
*/
public boolean isTXAware()
{
return true;
}
/**
* {@inheritDoc}
*/
public void onSaveItems(ItemStateChangesLog changesLog)
{
List<PlainChangesLog> chengesLogList = new ArrayList<PlainChangesLog>();
if (changesLog instanceof TransactionChangesLog)
{
ChangesLogIterator logIterator = ((TransactionChangesLog)changesLog).getLogIterator();
while (logIterator.hasNextLog())
{
chengesLogList.add(logIterator.nextLog());
}
}
else if (changesLog instanceof PlainChangesLog)
{
chengesLogList.add((PlainChangesLog)changesLog);
}
else if (changesLog instanceof CompositeChangesLog)
{
for (ChangesLogIterator iter = ((CompositeChangesLog)changesLog).getLogIterator(); iter.hasNextLog();)
{
chengesLogList.add(iter.nextLog());
}
}
List<LockOperationContainer> containers = new ArrayList<LockOperationContainer>();
for (PlainChangesLog currChangesLog : chengesLogList)
{
String sessionId = currChangesLog.getSessionId();
String nodeIdentifier;
try
{
switch (currChangesLog.getEventType())
{
case ExtendedEvent.LOCK :
if (currChangesLog.getSize() < 2)
{
LOG.error("Incorrect changes log of type ExtendedEvent.LOCK size=" + currChangesLog.getSize()
+ "<2 \n" + currChangesLog.dump());
break;
}
nodeIdentifier = currChangesLog.getAllStates().get(0).getData().getParentIdentifier();
CacheableSessionLockManager session = sessionLockManagers.get(sessionId);
if (session != null && session.containsPendingLock(nodeIdentifier))
{
containers.add(new LockOperationContainer(nodeIdentifier, currChangesLog.getSessionId(),
ExtendedEvent.LOCK));
}
else
{
LOG.error("Lock must exist in pending locks.");
}
break;
case ExtendedEvent.UNLOCK :
if (currChangesLog.getSize() < 2)
{
LOG.error("Incorrect changes log of type ExtendedEvent.UNLOCK size=" + currChangesLog.getSize()
+ "<2 \n" + currChangesLog.dump());
break;
}
containers.add(new LockOperationContainer(currChangesLog.getAllStates().get(0).getData()
.getParentIdentifier(), currChangesLog.getSessionId(), ExtendedEvent.UNLOCK));
break;
default :
HashSet<String> removedLock = new HashSet<String>();
for (ItemState itemState : currChangesLog.getAllStates())
{
// this is a node and node is locked
if (itemState.getData().isNode() && lockExist(itemState.getData().getIdentifier()))
{
nodeIdentifier = itemState.getData().getIdentifier();
if (itemState.isDeleted())
{
removedLock.add(nodeIdentifier);
}
else if (itemState.isAdded() || itemState.isRenamed() || itemState.isUpdated())
{
removedLock.remove(nodeIdentifier);
}
}
}
for (String identifier : removedLock)
{
containers.add(new LockOperationContainer(identifier, currChangesLog.getSessionId(),
ExtendedEvent.UNLOCK));
}
break;
}
}
catch (IllegalStateException e)
{
LOG.error(e.getLocalizedMessage(), e);
}
}
// sort locking and unlocking operations to avoid deadlocks
Collections.sort(containers);
for (LockOperationContainer container : containers)
{
try
{
container.apply();
}
catch (LockException e)
{
LOG.error(e.getMessage(), e);
}
}
}
/**
* Class containing operation type (LOCK or UNLOCK) and all the needed information like node uuid and session id.
*/
private class LockOperationContainer implements Comparable<LockOperationContainer>
{
private String identifier;
private String sessionId;
private int type;
/**
* @param identifier node identifier
* @param sessionId id of session
* @param type ExtendedEvent type specifying the operation (LOCK or UNLOCK)
*/
public LockOperationContainer(String identifier, String sessionId, int type)
{
super();
this.identifier = identifier;
this.sessionId = sessionId;
this.type = type;
}
/**
* @return node identifier
*/
public String getIdentifier()
{
return identifier;
}
public void apply() throws LockException
{
// invoke internalLock in LOCK operation
if (type == ExtendedEvent.LOCK)
{
internalLock(sessionId, identifier);
}
// invoke internalUnLock in UNLOCK operation
else if (type == ExtendedEvent.UNLOCK)
{
internalUnLock(sessionId, identifier);
}
}
/**
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(LockOperationContainer o)
{
return identifier.compareTo(o.getIdentifier());
}
}
/**
* Remove expired locks. Used from LockRemover.
*/
public synchronized void removeExpired()
{
final List<String> removeLockList = new ArrayList<String>();
for (LockData lock : getLockList())
{
if (!lock.isSessionScoped() && lock.getTimeToDeath() < 0)
{
removeLockList.add(lock.getNodeIdentifier());
}
}
Collections.sort(removeLockList);
for (String rLock : removeLockList)
{
removeLock(rLock);
}
}
/**
* {@inheritDoc}
*/
public void start()
{
lockRemover.start();
// Remove all locks records directly from DB.
boolean deleteLocks = "true".equalsIgnoreCase(PrivilegedSystemHelper.getProperty(LOCKS_FORCE_REMOVE, "false"));
if (deleteLocks)
{
doClean();
}
// remove all locks at the start up time EXOJCR-1592
// if (isCoordinator())
// {
// removeAll();
// }
}
/**
* {@inheritDoc}
*/
public void stop()
{
lockRemover.stop();
sessionLockManagers.clear();
}
/**
* Copy <code>PropertyData prop</code> to new TransientItemData
*
* @param prop
* @return
* @throws RepositoryException
*/
protected TransientItemData copyItemData(PropertyData prop) throws RepositoryException
{
if (prop == null)
{
return null;
}
// make a copy, value may be null for deleting items
TransientPropertyData newData =
new TransientPropertyData(prop.getQPath(), prop.getIdentifier(), prop.getPersistedVersion(), prop.getType(),
prop.getParentIdentifier(), prop.isMultiValued(), prop.getValues());
return newData;
}
/**
* Internal lock
*
* @param nodeIdentifier
* @throws LockException
*/
protected abstract void internalLock(String sessionId, String nodeIdentifier) throws LockException;
/**
* Internal unlock.
*
* @param sessionId
* @param nodeIdentifier
* @throws LockException
*/
protected abstract void internalUnLock(String sessionId, String nodeIdentifier) throws LockException;
/**
* {@inheritDoc}
*/
public String getLockTokenHash(String token)
{
String hash = "";
try
{
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(token.getBytes(), 0, token.length());
hash = new BigInteger(1, m.digest()).toString(16);
}
catch (NoSuchAlgorithmException e)
{
LOG.error("Can't get instanse of MD5 MessageDigest!", e);
}
return hash;
}
/**
* {@inheritDoc}
*/
public LockData getExactNodeOrCloseParentLock(NodeData node) throws RepositoryException
{
return getExactNodeOrCloseParentLock(node, true);
}
private LockData getExactNodeOrCloseParentLock(NodeData node, boolean checkHasLocks) throws RepositoryException
{
if (node == null || (checkHasLocks && !hasLocks()))
{
return null;
}
LockData retval = null;
retval = getLockDataById(node.getIdentifier());
if (retval == null)
{
NodeData parentData = (NodeData)dataManager.getItemData(node.getParentIdentifier());
if (parentData != null)
{
retval = getExactNodeOrCloseParentLock(parentData, false);
}
}
return retval;
}
/**
* {@inheritDoc}
*/
public LockData getExactNodeLock(NodeData node) throws RepositoryException
{
if (node == null || !hasLocks())
{
return null;
}
return getLockDataById(node.getIdentifier());
}
/**
* {@inheritDoc}
*/
public LockData getClosedChild(NodeData node) throws RepositoryException
{
return getClosedChild(node, true);
}
private LockData getClosedChild(NodeData node, boolean checkHasLocks) throws RepositoryException
{
if (node == null || (checkHasLocks && !hasLocks()))
{
return null;
}
LockData retval = null;
List<NodeData> childData = dataManager.getChildNodesData(node);
for (NodeData nodeData : childData)
{
retval = getLockDataById(nodeData.getIdentifier());
if (retval != null)
{
return retval;
}
}
// child not found try to find dipper
for (NodeData nodeData : childData)
{
retval = getClosedChild(nodeData, false);
if (retval != null)
{
return retval;
}
}
return retval;
}
/**
* Remove lock, used by Lock remover.
*
* @param nodeIdentifier String
*/
protected void removeLock(String nodeIdentifier)
{
try
{
NodeData nData = (NodeData)dataManager.getItemData(nodeIdentifier);
//Skip removing, because that node was removed in other node of cluster.
if (nData == null)
{
return;
}
PlainChangesLog changesLog =
new PlainChangesLogImpl(new ArrayList<ItemState>(), IdentityConstants.SYSTEM, ExtendedEvent.UNLOCK);
ItemData lockOwner =
copyItemData((PropertyData)dataManager.getItemData(nData, new QPathEntry(Constants.JCR_LOCKOWNER, 1),
ItemType.PROPERTY));
//Skip removing, because that lock was removed in other node of cluster.
if (lockOwner == null)
{
return;
}
changesLog.add(ItemState.createDeletedState(lockOwner));
ItemData lockIsDeep =
copyItemData((PropertyData)dataManager.getItemData(nData, new QPathEntry(Constants.JCR_LOCKISDEEP, 1),
ItemType.PROPERTY));
//Skip removing, because that lock was removed in other node of cluster.
if (lockIsDeep == null)
{
return;
}
changesLog.add(ItemState.createDeletedState(lockIsDeep));
// lock probably removed by other thread
if (lockOwner == null && lockIsDeep == null)
{
return;
}
dataManager.save(new TransactionChangesLog(changesLog));
}
catch (JCRInvalidItemStateException e)
{
//Skip property not found in DB, because that lock property was removed in other node of cluster.
if (LOG.isDebugEnabled())
{
LOG.debug("The propperty was removed in other node of cluster.", e);
}
}
catch (RepositoryException e)
{
LOG.error("Error occur during removing lock" + e.getLocalizedMessage(), e);
}
}
/**
* {@inheritDoc}
*/
public void closeSessionLockManager(String sessionID)
{
sessionLockManagers.remove(sessionID);
}
/**
* 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 LockActionNonTxAware<R, A> extends ActionNonTxAware<R, A, LockException>
{
/**
* @see org.exoplatform.services.transaction.ActionNonTxAware#getTransactionManager()
*/
protected TransactionManager getTransactionManager()
{
return tm;
}
}
/**
* Remove all locks.
*/
protected void removeAll()
{
List<LockData> locks = getLockList();
for (LockData lockData : locks)
{
removeLock(lockData.getNodeIdentifier());
}
}
/**
* {@inheritDoc}
*/
public void clean() throws BackupException
{
LOG.info("Start to clean lock Data");
doClean();
}
/**
* {@inheritDoc}
*/
public void backup(File storageDir) throws BackupException
{
LOG.info("Start to backup lock data");
ObjectOutputStream out = null;
try
{
File contentFile = new File(storageDir, "CacheLocks" + DBBackup.CONTENT_FILE_SUFFIX);
out = new ObjectOutputStream(new BufferedOutputStream(PrivilegedFileHelper.fileOutputStream(contentFile)));
List<LockData> locks = getLockList();
out.writeInt(locks.size());
for (LockData lockData : locks)
{
lockData.writeExternal(out);
}
}
catch (FileNotFoundException e)
{
throw new BackupException(e);
}
catch (IOException e)
{
throw new BackupException(e);
}
finally
{
if (out != null)
{
try
{
out.flush();
out.close();
}
catch (IOException e)
{
LOG.error("Can't close output stream", e);
}
}
}
}
/**
* {@inheritDoc}
*/
public DataRestore getDataRestorer(DataRestoreContext context) throws BackupException
{
List<LockData> locks = new ArrayList<LockData>();
ObjectInputStream in = null;
try
{
File contentFile = new File((File)context.getObject(DataRestoreContext.STORAGE_DIR),
"CacheLocks" + DBBackup.CONTENT_FILE_SUFFIX);
// it is possible that backup was created on configuration without Backupable WorkspaceLockManager class
if (!PrivilegedFileHelper.exists(contentFile))
{
return new DummyDataRestore();
}
in = new ObjectInputStream(PrivilegedFileHelper.fileInputStream(contentFile));
int count = in.readInt();
for (int i = 0; i < count; i++)
{
LockData lockData = new LockData();
lockData.readExternal(in);
locks.add(lockData);
}
}
catch (FileNotFoundException e)
{
throw new BackupException(e);
}
catch (IOException e)
{
throw new BackupException(e);
}
catch (ClassNotFoundException e)
{
throw new BackupException(e);
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException e)
{
LOG.error("Can't close output stream", e);
}
}
}
return new CacheLocksRestore(locks);
}
/**
* Cache restorer.
*/
protected class CacheLocksRestore implements DataRestore
{
final private List<LockData> backupLocks = new ArrayList<LockData>();
private List<LockData> actualLocks = new ArrayList<LockData>();
CacheLocksRestore(final List<LockData> backupLocks)
{
this.backupLocks.addAll(backupLocks);
}
/**
* {@inheritDoc}
*/
public void clean() throws BackupException
{
LOG.info("Start to clean lock Data");
SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
{
public Void run()
{
actualLocks.addAll(getLockList());
return null;
}
});
doClean();
}
/**
* {@inheritDoc}
*/
public void restore() throws BackupException
{
for (LockData lockData : backupLocks)
{
doPut(lockData);
}
}
/**
* {@inheritDoc}
*/
public void commit() throws BackupException
{
}
/**
* {@inheritDoc}
*/
public void rollback() throws BackupException
{
doClean();
for (LockData lockData : actualLocks)
{
doPut(lockData);
}
}
/**
* {@inheritDoc}
*/
public void close() throws BackupException
{
backupLocks.clear();
actualLocks.clear();
}
}
/**
* Indicates if we are the only node in cluster.
*/
protected abstract boolean isAloneInCluster();
/**
* Puts lock data directly into cache.
*
* @param lockData
* the lock data to put
* @return the old lock data previously added into cache
*/
protected abstract LockData doPut(LockData lockData);
/**
* Removes lock data directly from cache.
*
* @param lockData
* the lock data to remove
*/
protected abstract void doRemove(LockData lockData);
/**
* Clean cache directly.
*/
protected abstract void doClean();
/**
* Returns {@link LockTableHandler} for lock table consistency checking.
*/
public abstract LockTableHandler getLockTableHandler();
}