/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.core.lock;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.WorkspaceImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.data.core.TransactionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import javax.jcr.Workspace;
import javax.jcr.lock.LockException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
/**
* Encapsulates operations that happen in an XA environment.
*/
class XAEnvironment {
/**
* Logger instance for this class
*/
private static final Logger log = LoggerFactory.getLogger(XAEnvironment.class);
private static final int STATUS_PREPARING = 1;
private static final int STATUS_PREPARED = 2;
private static final int STATUS_COMMITTING = 3;
private static final int STATUS_COMMITTED = 4;
private static final int STATUS_ROLLING_BACK = 5;
private static final int STATUS_ROLLED_BACK = 6;
/**
* Global lock manager.
*/
private final LockManagerImpl lockMgr;
/**
* Map of locked nodes, indexed by their (internal) id.
*/
private final Map<NodeId, XALockInfo> lockedNodesMap =
new HashMap<NodeId, XALockInfo>();
/**
* Map of unlocked nodes, indexed by their (internal) id.
*/
private final Map<NodeId, XALockInfo> unlockedNodesMap =
new HashMap<NodeId, XALockInfo>();
/**
* List of lock/unlock operations.
*/
private final List<XALockInfo> operations = new ArrayList<XALockInfo>();
/**
* Operation index.
*/
private int opIndex;
/**
* Current status.
*/
private int status;
/**
* Create a new instance of this class.
* @param lockMgr global lock manager
*/
public XAEnvironment(LockManagerImpl lockMgr) {
this.lockMgr = lockMgr;
}
/**
* Reset this environment.
*/
public void reset() {
lockedNodesMap.clear();
unlockedNodesMap.clear();
operations.clear();
opIndex = 0;
}
/**
* Lock some node.
* @param node node to lock
* @param isDeep <code>true</code> to deep lock this node;
* <code>false</code> otherwise
* @param isSessionScoped <code>true</code> if lock should be session scoped;
* <code>false</code> otherwise
* @throws LockException if node is already locked
* @throws RepositoryException if an error occurs
*/
public LockInfo lock(NodeImpl node, boolean isDeep, boolean isSessionScoped)
throws LockException, RepositoryException {
return lock(node, isDeep, isSessionScoped, Long.MAX_VALUE, null);
}
/**
* Lock some node.
* @param node node to lock
* @param isDeep <code>true</code> to deep lock this node;
* <code>false</code> otherwise
* @param isSessionScoped <code>true</code> if lock should be session scoped;
* <code>false</code> otherwise
* @param timeoutHint
* @param ownerInfo
* @throws LockException if node is already locked
* @throws RepositoryException if an error occurs
*/
public LockInfo lock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerInfo)
throws LockException, RepositoryException {
NodeId id = node.getNodeId();
// check negative set first
XALockInfo info = unlockedNodesMap.get(id);
if (info != null) {
// if settings are compatible, this is effectively a no-op
if (info.isDeep() == isDeep && info.isSessionScoped() == isSessionScoped) {
unlockedNodesMap.remove(id);
operations.remove(info);
return lockMgr.getLockInfo(id);
}
}
// verify node is not already locked.
if (isLocked(node)) {
throw new LockException("Node locked.");
}
// create a new lock info for this node
String lockOwner = (ownerInfo != null) ? ownerInfo : node.getSession().getUserID();
info = new XALockInfo(node, isSessionScoped, isDeep, timeoutHint, lockOwner);
SessionImpl session = (SessionImpl) node.getSession();
info.setLockHolder(session);
info.setLive(true);
LockManagerImpl.getSessionLockManager(session).lockTokenAdded(info.getLockToken());
lockedNodesMap.put(id, info);
operations.add(info);
return info;
}
/**
* Unlock some node.
* @param node node to unlock
* @throws LockException if the node is not locked
* @throws RepositoryException if an error occurs
*/
public void unlock(NodeImpl node) throws LockException, RepositoryException {
NodeId id = node.getNodeId();
// check positive set first
LockInfo info = lockedNodesMap.get(id);
if (info != null) {
lockedNodesMap.remove(id);
operations.remove(info);
info.setLive(false);
} else {
info = getLockInfo(node);
if (info == null || !info.getId().equals(id)) {
throw new LockException("Node not locked.");
} else if (!info.isLockHolder(node.getSession())) {
throw new LockException("Node not locked by this session.");
}
XALockInfo xaInfo = new XALockInfo(node, info);
unlockedNodesMap.put(id, xaInfo);
operations.add(xaInfo);
}
}
/**
* Return a flag indicating whether the specified node is locked.
* @return <code>true</code> if this node is locked;
* <code>false</code> otherwise
* @throws RepositoryException if an error occurs
*/
public boolean isLocked(NodeImpl node) throws RepositoryException {
return getLockInfo(node) != null;
}
/**
* Return the most appropriate lock information for a node. This is either
* the lock info for the node itself, if it is locked, or a lock info for
* one of its parents, if that one is deep locked.
* @param node node
* @return LockInfo lock info or <code>null</code> if node is not locked
* @throws RepositoryException if an error occurs
*/
public LockInfo getLockInfo(NodeImpl node) throws RepositoryException {
NodeId id = node.getNodeId();
// check negative set
if (unlockedNodesMap.containsKey(id)) {
return null;
}
// check positive set, iteratively ascending in hierarchy
if (!lockedNodesMap.isEmpty()) {
NodeImpl current = node;
for (;;) {
XALockInfo info = lockedNodesMap.get(current.getId());
if (info != null) {
if (info.getId().equals(id) || info.isDeep()) {
return info;
}
break;
}
if (current.getDepth() == 0) {
break;
}
current = (NodeImpl) current.getParent();
}
}
// ask parent
return lockMgr.getLockInfo(id);
}
/**
* Returns all locks associated with the specified session.
* @param session session
* @return locks associated with the session
* @throws RepositoryException if an error occurs
*/
public LockInfo[] getLockInfos(SessionImpl session)
throws RepositoryException {
ArrayList<LockInfo> result = new ArrayList<LockInfo>();
// get lock information from global lock manager first
for (LockInfo info : lockMgr.getLockInfos(session)) {
// check negative set
if (!unlockedNodesMap.containsKey(info.getId())) {
result.add(info);
}
}
// add 'uncommitted' lock information
result.addAll(lockedNodesMap.values());
return result.toArray(new LockInfo[result.size()]);
}
/**
* Add lock token to this environment.
* @param session
* @param lt lock token
* @throws RepositoryException
*/
public void addLockToken(SessionImpl session, String lt) throws RepositoryException {
try {
NodeId id = LockInfo.parseLockToken(lt);
NodeImpl node = (NodeImpl) session.getItemManager().getItem(id);
LockInfo info = getLockInfo(node);
if (info != null && !info.isLockHolder(session)) {
if (info.getLockHolder() == null) {
info.setLockHolder(session);
} else {
String msg = "Cannot add lock token: lock already held by other session.";
log.warn(msg);
throw new LockException(msg);
}
}
// inform SessionLockManager
getSessionLockManager(session).lockTokenAdded(lt);
} catch (IllegalArgumentException e) {
String msg = "Bad lock token: " + e.getMessage();
log.warn(msg);
throw new LockException(msg);
}
}
/**
* Remove lock token from this environment.
* @param session
* @param lt lock token
* @throws RepositoryException
*/
public void removeLockToken(SessionImpl session, String lt) throws RepositoryException {
try {
NodeId id = LockInfo.parseLockToken(lt);
NodeImpl node = (NodeImpl) session.getItemManager().getItem(id);
LockInfo info = getLockInfo(node);
if (info != null) {
if (info.isLockHolder(session)) {
info.setLockHolder(null);
} else if (info.getLockHolder() != null) {
String msg = "Cannot remove lock token: lock held by other session.";
log.warn(msg);
throw new LockException(msg);
}
}
// inform SessionLockManager
getSessionLockManager(session).lockTokenRemoved(lt);
} catch (IllegalArgumentException e) {
String msg = "Bad lock token: " + e.getMessage();
log.warn(msg);
throw new LockException(msg);
}
}
static SessionLockManager getSessionLockManager(SessionImpl session) throws RepositoryException {
Workspace wsp = session.getWorkspace();
return (SessionLockManager) wsp.getLockManager();
}
/**
* Prepare update. Locks global lock manager and feeds all lock/
* unlock operations.
*/
public void prepare() throws TransactionException {
status = STATUS_PREPARING;
if (!operations.isEmpty()) {
lockMgr.beginUpdate();
try {
while (opIndex < operations.size()) {
try {
XALockInfo info = operations.get(opIndex);
info.update();
} catch (RepositoryException e) {
throw new TransactionException("Unable to update.", e);
}
opIndex++;
}
} finally {
if (opIndex < operations.size()) {
while (opIndex > 0) {
try {
XALockInfo info = operations.get(opIndex - 1);
info.undo();
} catch (RepositoryException e) {
log.error("Unable to undo lock operation.", e);
}
opIndex--;
}
lockMgr.cancelUpdate();
}
}
}
status = STATUS_PREPARED;
}
/**
* Commit changes. This will finish the update and unlock the
* global lock manager.
*/
public void commit() {
int oldStatus = status;
status = STATUS_COMMITTING;
if (oldStatus == STATUS_PREPARED) {
if (!operations.isEmpty()) {
lockMgr.endUpdate();
reset();
}
}
status = STATUS_COMMITTED;
}
/**
* Rollback changes. This will undo all updates and unlock the
* global lock manager.
*/
public void rollback() {
int oldStatus = status;
status = STATUS_ROLLING_BACK;
if (oldStatus == STATUS_PREPARED) {
if (!operations.isEmpty()) {
while (opIndex > 0) {
try {
XALockInfo info = operations.get(opIndex - 1);
info.undo();
} catch (RepositoryException e) {
log.error("Unable to undo lock operation.", e);
}
opIndex--;
}
lockMgr.cancelUpdate();
reset();
}
}
status = STATUS_ROLLED_BACK;
}
/**
* Return a flag indicating whether a lock info belongs to a different
* XA environment.
*/
public boolean differentXAEnv(LockInfo info) {
if (info instanceof XALockInfo) {
XALockInfo lockInfo = (XALockInfo) info;
return lockInfo.getXAEnv() != this;
}
return true;
}
/**
* Information about a lock used inside transactions.
*/
class XALockInfo extends LockInfo {
/**
* Node being locked/unlocked.
*/
private final NodeImpl node;
/**
* Flag indicating whether this info belongs to a unlock operation.
*/
private boolean isUnlock;
/**
* Create a new instance of this class.
* @param sessionScoped whether lock token is session scoped
* @param deep whether lock is deep
* @param lockOwner owner of lock
*/
public XALockInfo(
NodeImpl node,
boolean sessionScoped, boolean deep, long timeoutHint, String lockOwner) {
super(node.getNodeId(), sessionScoped, deep, lockOwner, timeoutHint);
this.node = node;
}
/**
* Create a new instance of this class. Used to signal an
* unlock operation on some existing lock information.
*/
public XALockInfo(NodeImpl node, LockInfo info) {
super(info);
this.node = node;
this.isUnlock = true;
}
/**
* Return a flag indicating whether this info belongs to a unlock operation.
* @return <code>true</code> if this info belongs to an unlock operation;
* otherwise <code>false</code>
*/
public boolean isUnlock() {
return isUnlock;
}
/**
* Do operation.
*/
public void update() throws LockException, RepositoryException {
if (isUnlock) {
// Only if we have a valid ItemState try to unlock
// JCR-2332
if (((WorkspaceImpl) node.getSession().getWorkspace()).getItemStateManager().hasItemState(node.getId())) {
lockMgr.internalUnlock(node);
}
} else {
LockInfo internalLock = lockMgr.internalLock(
node, isDeep(), isSessionScoped(),
getTimeoutHint(),
// getTimeoutTime(),
getLockOwner());
LockInfo xaEnvLock = getLockInfo(node);
// Check if the lockToken has been removed in the transaction ...
if (xaEnvLock != null && xaEnvLock.getLockHolder() == null) {
//Remove lockToken from SessionLockManager
getSessionLockManager(internalLock.getLockHolder()).lockTokenRemoved(internalLock.getLockToken());
internalLock.setLockHolder(null);
}
}
}
/**
* Undo operation.
*/
public void undo() throws LockException, RepositoryException {
if (isUnlock) {
lockMgr.internalLock(
node, isDeep(), isSessionScoped(),
getTimeoutHint(), getLockOwner());
} else {
lockMgr.internalUnlock(node);
}
}
/**
* Return parent environment.
*/
public XAEnvironment getXAEnv() {
return XAEnvironment.this;
}
/**
* {@inheritDoc}
* <p>
* As long as the XA environment is neither committed nor rolled back,
* associated lock information is subject to change.
*/
@Override
public boolean mayChange() {
if (status != STATUS_COMMITTED
&& status != STATUS_ROLLED_BACK) {
return true;
}
return super.mayChange();
}
}
}