/*
* Copyright (C) 2012 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.quota;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.ExtendedMandatoryItemsPersistenceListener;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.jcr.impl.quota.BaseQuotaManager.ExceededQuotaBehavior;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.rpc.RPCException;
import org.exoplatform.services.rpc.RPCService;
import org.exoplatform.services.rpc.RemoteCommand;
import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import javax.jcr.RepositoryException;
/**
* {@link ExtendedMandatoryItemsPersistenceListener} implementation.
*
* Is TX aware listener. Receive changes before data is committed to storage.
* It allows to validate if some entity can exceeds quota limit if new changes
* is coming.
*
* @author <a href="abazko@exoplatform.com">Anatoliy Bazko</a>
* @version $Id: ChangesListener.java 34360 2009-07-22 23:58:59Z tolusha $
*/
public class ChangesListener implements ExtendedMandatoryItemsPersistenceListener
{
/**
* Logger.
*/
protected final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.ChangesListener");
/**
* Pending changes of current save. If save failed changes will be removed, otherwise
* is moved into changes log to be pushed to coordinator by timer.
*/
protected ThreadLocal<ChangesItem> pendingChanges = new ThreadLocal<ChangesItem>();
/**
* Accumulates changes of every save.
*/
protected ChangesLog changesLog = new ChangesLog();
/**
* {@link WorkspaceQuotaManager} instance.
*/
protected final WorkspaceQuotaManager wqm;
/**
* {@link RPCService}
*/
protected final RPCService rpcService;
/**
* Workspace name.
*/
protected final String wsName;
/**
* Repository name.
*/
protected final String rName;
/**
* Unique name.
*/
protected final String uniqueName;
/**
* {@link QuotaPersister}
*/
protected final QuotaPersister quotaPersister;
/**
* Remote command is obligated to apply changes log at coordinator.
*/
protected RemoteCommand applyPersistedChangesTask;
/**
* @see WorkspaceQuotaContext#executor
*/
protected final ExecutorService executor;
/**
* @see ExceededQuotaLimitException
*/
protected final ExceededQuotaBehavior exceededQuotaBehavior;
/**
* {@link LocationFactory} instance.
*/
protected final LocationFactory lFactory;
/**
* ChangesListener constructor.
*/
ChangesListener(WorkspaceQuotaManager wqm)
{
this.wqm = wqm;
this.rpcService = wqm.getContext().rpcService;
this.wsName = wqm.getContext().wsName;
this.rName = wqm.getContext().rName;
this.uniqueName = wqm.getContext().uniqueName;
this.quotaPersister = wqm.getContext().quotaPersister;
this.executor = wqm.getContext().executor;
this.exceededQuotaBehavior = wqm.getContext().exceededQuotaBehavior;
this.lFactory = wqm.getContext().lFactory;
initApplyPersistedChangesTask();
rpcService.registerCommand(applyPersistedChangesTask);
}
/**
* {@inheritDoc}
*
* Checks if new changes can exceeds some limits. It either can be node, workspace,
* repository or global JCR instance.
*
* @throws IllegalStateException if data size exceeded quota limit
*/
public void onSaveItems(ItemStateChangesLog itemStates)
{
try
{
ChangesItem changesItem = new ChangesItem();
for (ItemState state : itemStates.getAllStates())
{
if (!state.getData().isNode())
{
String nodePath = getPath(state.getData().getQPath().makeParentPath());
Set<String> parentsWithQuota = quotaPersister.getAllParentNodesWithQuota(rName, wsName, nodePath);
for (String parent : parentsWithQuota)
{
changesItem.updateNodeChangedSize(parent, state.getChangedSize());
addPathsWithAsyncUpdate(changesItem, parent);
}
changesItem.updateWorkspaceChangedSize(state.getChangedSize());
}
else
{
addPathsWithUnknownChangedSize(changesItem, state);
}
}
validatePendingChanges(changesItem);
pendingChanges.set(changesItem);
}
catch (ExceededQuotaLimitException e)
{
throw new IllegalStateException(e.getMessage(), e);
}
}
/**
* Checks if changes were made but changed size is unknown. If so, determinate
* for which nodes data size should be recalculated at all and put those paths into
* respective collection.
*/
private void addPathsWithUnknownChangedSize(ChangesItem changesItem, ItemState state)
{
if (!state.isPersisted() && (state.isDeleted() || state.isRenamed()))
{
String itemPath = getPath(state.getData().getQPath());
for (String trackedPath : quotaPersister.getAllTrackedNodes(rName, wsName))
{
if (itemPath.startsWith(trackedPath))
{
changesItem.addPathWithUnknownChangedSize(itemPath);
}
}
}
}
/**
* Checks if data size for node is represented by <code>quotableParent</code> path
* should be updated asynchronously. If so that path is putting into respective collection.
*
* @param quotableParent
* absolute path to node for which quota is set
*/
private void addPathsWithAsyncUpdate(ChangesItem changesItem, String quotableParent)
{
boolean isAsyncUpdate;
try
{
isAsyncUpdate = quotaPersister.isNodeQuotaOrGroupOfNodesQuotaAsync(rName, wsName, quotableParent);
}
catch (UnknownQuotaLimitException e)
{
isAsyncUpdate = true;
}
if (isAsyncUpdate)
{
changesItem.addPathWithAsyncUpdate(quotableParent);
}
}
/**
* Checks if entities can accept new changes. It is not possible when
* current behavior is {@link ExceededQuotaBehavior#EXCEPTION} and
* new data size exceeds quota limit.
*
* @throws ExceededQuotaLimitException if new data size exceeds quota limit
*/
private void validatePendingChanges(ChangesItem changesItem) throws ExceededQuotaLimitException
{
long delta = changesItem.getWorkspaceChangedSize() + changesLog.getWorkspaceChangedSize();
if (delta > 0)
{
validatePendingWorkspaceChanges(delta);
validatePendingRepositoryChanges(delta);
validatePendingGlobalChanges(delta);
}
validatePendingNodesChanges(changesItem.getAllNodesCalculatedChangedSize());
}
/**
* @see #validatePendingChanges(ChangesItem)
*/
private void validatePendingWorkspaceChanges(long delta) throws ExceededQuotaLimitException
{
try
{
long quotaLimit = quotaPersister.getWorkspaceQuota(rName, wsName);
try
{
long dataSize = quotaPersister.getWorkspaceDataSize(rName, wsName);
if (dataSize + delta > quotaLimit)
{
behaveWhenQuotaExceeded("In workspace '" + wqm.uniqueName + "' data size exceeded quota limit");
}
}
catch (UnknownDataSizeException e)
{
return;
}
}
catch (UnknownQuotaLimitException e)
{
return;
}
}
/**
* @see #validatePendingChanges(ChangesItem)
*/
private void validatePendingRepositoryChanges(long delta) throws ExceededQuotaLimitException
{
try
{
long quotaLimit = quotaPersister.getRepositoryQuota(rName);
try
{
long dataSize = quotaPersister.getRepositoryDataSize(rName);
if (dataSize + delta > quotaLimit)
{
behaveWhenQuotaExceeded("In repository '" + rName + "' data size exceeded quota limit");
}
}
catch (UnknownDataSizeException e)
{
return;
}
}
catch (UnknownQuotaLimitException e)
{
return;
}
}
/**
* @see #validatePendingChanges(ChangesItem)
*/
private void validatePendingGlobalChanges(long delta) throws ExceededQuotaLimitException
{
try
{
long quotaLimit = quotaPersister.getGlobalQuota();
try
{
long dataSize = quotaPersister.getGlobalDataSize();
if (dataSize + delta > quotaLimit)
{
behaveWhenQuotaExceeded("Global data size exceeded quota limit");
}
}
catch (UnknownDataSizeException e)
{
return;
}
}
catch (UnknownQuotaLimitException e)
{
return;
}
}
/**
* @see #validatePendingChanges(ChangesItem)
*/
private void validatePendingNodesChanges(Map<String, Long> calculatedNodesChangedSize)
throws ExceededQuotaLimitException
{
for (Entry<String, Long> entry : calculatedNodesChangedSize.entrySet())
{
String nodePath = entry.getKey();
long delta = entry.getValue() + changesLog.getNodeChangedSize(nodePath);
if (delta > 0)
{
try
{
long dataSize = quotaPersister.getNodeDataSize(rName, wsName, nodePath);
try
{
long quotaLimit = quotaPersister.getNodeQuotaOrGroupOfNodesQuota(rName, wsName, nodePath);
if (dataSize + delta > quotaLimit)
{
behaveWhenQuotaExceeded("Node '" + nodePath + "' data size exceeded quota limit");
}
}
catch (UnknownQuotaLimitException e)
{
continue;
}
}
catch (UnknownDataSizeException e)
{
continue;
}
}
}
}
/**
* What to do if data size exceeded quota limit. Throwing exception or logging only.
* Depends on preconfigured parameter.
*
* @param message
* the detail message for exception or log operation
* @throws ExceededQuotaLimitException
* if current behavior is {@link ExceededQuotaBehavior#EXCEPTION}
*/
private void behaveWhenQuotaExceeded(String message) throws ExceededQuotaLimitException
{
switch (exceededQuotaBehavior)
{
case EXCEPTION :
throw new ExceededQuotaLimitException(message);
case WARNING :
LOG.warn(message);
break;
}
}
/**
* {@inheritDoc}
*/
public void onCommit()
{
ChangesItem changesItem = pendingChanges.get();
try
{
pushChangesToCoordinator(changesItem.extractSyncChanges());
changesLog.add(changesItem);
}
catch (SecurityException e)
{
throw new IllegalStateException("Can't push changes to coordinator", e.getCause());
}
catch (RPCException e)
{
throw new IllegalStateException("Can't push changes to coordinator", e.getCause());
}
finally
{
pendingChanges.remove();
}
}
/**
* Push all changes to coordinator to apply.
*/
protected void pushAllChangesToCoordinator() throws SecurityException, RPCException
{
ChangesItem changesItem = changesLog.pollAndMergeAll();
pushChangesToCoordinator(changesItem);
}
/**
* Push changes to coordinator to apply.
*/
protected void pushChangesToCoordinator(ChangesItem changesItem) throws SecurityException, RPCException
{
if (!changesItem.isEmpty())
{
rpcService.executeCommandOnCoordinator(applyPersistedChangesTask, true, changesItem);
}
}
/**
* {@inheritDoc}
*/
public void onRollback()
{
pendingChanges.remove();
}
/**
* {@inheritDoc}
*/
public boolean isTXAware()
{
return true;
}
/**
* Returns item absolute path.
*
* @param path
* {@link QPath} representation
* @throws IllegalStateException if something wrong
*/
private String getPath(QPath path)
{
try
{
return lFactory.createJCRPath(path).getAsString(false);
}
catch (RepositoryException e)
{
throw new IllegalStateException(e.getMessage(), e);
}
}
/**
* Free all allocated resources.
*/
public void destroy()
{
rpcService.unregisterCommand(applyPersistedChangesTask);
}
/**
* Initialize remote command {@link #applyPersistedChangesTask}
*/
private void initApplyPersistedChangesTask()
{
applyPersistedChangesTask = new RemoteCommand()
{
/**
* {@inheritDoc}
*/
public String getId()
{
return "ChangesListener-" + uniqueName + "-applyPersistedChangesTask";
}
/**
* Accumulates persisted changes.
*/
public Serializable execute(final Serializable[] args) throws Throwable
{
ChangesItem changesItem = (ChangesItem)args[0];
Runnable task = new ApplyPersistedChangesTask(wqm.getContext(), changesItem);
executor.execute(task);
return null;
}
};
}
}