org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jackrabbit-jcr-server Show documentation
Show all versions of jackrabbit-jcr-server Show documentation
WebDAV server implementations for JCR
/*
* 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.webdav.jcr.transaction;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.webdav.DavConstants;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.apache.jackrabbit.webdav.jcr.JcrDavException;
import org.apache.jackrabbit.webdav.jcr.JcrDavSession;
import org.apache.jackrabbit.webdav.lock.ActiveLock;
import org.apache.jackrabbit.webdav.lock.LockInfo;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.Scope;
import org.apache.jackrabbit.webdav.lock.Type;
import org.apache.jackrabbit.webdav.transaction.TransactionConstants;
import org.apache.jackrabbit.webdav.transaction.TransactionInfo;
import org.apache.jackrabbit.webdav.transaction.TransactionResource;
import org.apache.jackrabbit.webdav.transaction.TxActiveLock;
import org.apache.jackrabbit.webdav.transaction.TxLockManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Item;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.util.HashMap;
import java.util.Iterator;
import java.util.IdentityHashMap;
import java.util.Map;
/**
* TxLockManagerImpl
manages locks with locktype
* '{@link TransactionConstants#TRANSACTION dcr:transaction}'.
*
*/
//todo: removing all expired locks
//todo: 'local' and 'global' are not accurate terms in the given context > replace
/*todo: the usage of the 'global' transaction is not according to the JTA specification,
which explicitly requires any transaction present on a servlet to be completed before
the service method returns. Starting/completing transactions on the session object,
which is possible with the jackrabbit implementation is a hack.*/
/*todo: review of this transaction part is therefore required. Is there a use-case
for those 'global' transactions at all...*/
public class TxLockManagerImpl implements TxLockManager {
private static Logger log = LoggerFactory.getLogger(TxLockManagerImpl.class);
private final TransactionMap map = new TransactionMap();
private final Map listeners = new IdentityHashMap();
/**
* Create a new lock.
*
* @param lockInfo as present in the request body.
* @param resource
* @return the lock
* @throws DavException if the lock could not be obtained.
* @throws IllegalArgumentException if the resource is null
or
* does not implement {@link TransactionResource} interface.
* @see LockManager#createLock(org.apache.jackrabbit.webdav.lock.LockInfo, org.apache.jackrabbit.webdav.DavResource)
*/
public ActiveLock createLock(LockInfo lockInfo, DavResource resource)
throws DavException {
if (resource == null || !(resource instanceof TransactionResource)) {
throw new IllegalArgumentException("Invalid resource");
}
return createLock(lockInfo, (TransactionResource) resource);
}
/**
* Create a new lock.
*
* @param lockInfo
* @param resource
* @return the lock
* @throws DavException if the request lock has the wrong lock type or if
* the lock could not be obtained for any reason.
*/
private synchronized ActiveLock createLock(LockInfo lockInfo, TransactionResource resource)
throws DavException {
if (!lockInfo.isDeep() || !TransactionConstants.TRANSACTION.equals(lockInfo.getType())) {
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED);
}
ActiveLock existing = getLock(lockInfo.getType(), lockInfo.getScope(), resource);
if (existing != null) {
throw new DavException(DavServletResponse.SC_LOCKED);
}
// TODO: check for locks on member resources is required as well for lock is always deep!
Transaction tx = createTransaction(resource.getLocator(), lockInfo);
tx.start(resource);
// keep references to this lock
addReferences(tx, getMap(resource), resource);
return tx.getLock();
}
/**
* Build the transaction object associated by the lock.
*
* @param locator
* @param lockInfo
* @return
*/
private Transaction createTransaction(DavResourceLocator locator, LockInfo lockInfo) {
if (TransactionConstants.GLOBAL.equals(lockInfo.getScope())) {
return new GlobalTransaction(locator, new TxActiveLock(lockInfo));
} else {
return new LocalTransaction(locator, new TxActiveLock(lockInfo));
}
}
/**
* Refresh the lock identified by the given lock token.
*
* @param lockInfo
* @param lockToken
* @param resource
* @return the lock
* @throws DavException
* @throws IllegalArgumentException if the resource is null
or
* does not implement {@link TransactionResource} interface.
* @see LockManager#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String, org.apache.jackrabbit.webdav.DavResource)
*/
public ActiveLock refreshLock(LockInfo lockInfo, String lockToken,
DavResource resource) throws DavException {
if (resource == null || !(resource instanceof TransactionResource)) {
throw new IllegalArgumentException("Invalid resource");
}
return refreshLock(lockInfo, lockToken, (TransactionResource) resource);
}
/**
* Reset the timeout of the lock identified by the given lock token.
*
* @param lockInfo
* @param lockToken
* @param resource
* @return
* @throws DavException if the lock did not exist or is expired.
*/
private synchronized ActiveLock refreshLock(LockInfo lockInfo, String lockToken,
TransactionResource resource) throws DavException {
TransactionMap responsibleMap = getMap(resource);
Transaction tx = responsibleMap.get(lockToken);
if (tx == null) {
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No valid transaction lock found for resource '" + resource.getResourcePath() + "'");
} else if (tx.getLock().isExpired()) {
removeExpired(tx, responsibleMap, resource);
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction lock for resource '" + resource.getResourcePath() + "' was already expired.");
} else {
tx.getLock().setTimeout(lockInfo.getTimeout());
}
return tx.getLock();
}
/**
* Throws UnsupportedOperationException.
*
* @param lockToken
* @param resource
* @throws DavException
* @see LockManager#releaseLock(String, org.apache.jackrabbit.webdav.DavResource)
*/
public void releaseLock(String lockToken, DavResource resource)
throws DavException {
throw new UnsupportedOperationException("A transaction lock can only be release with a TransactionInfo object and a lock token.");
}
/**
* Release the lock identified by the given lock token.
*
* @param lockInfo
* @param lockToken
* @param resource
* @throws DavException
*/
public synchronized void releaseLock(TransactionInfo lockInfo, String lockToken,
TransactionResource resource) throws DavException {
if (resource == null) {
throw new IllegalArgumentException("Resource must not be null.");
}
TransactionMap responsibleMap = getMap(resource);
Transaction tx = responsibleMap.get(lockToken);
if (tx == null) {
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No transaction lock found for resource '" + resource.getResourcePath() + "'");
} else if (tx.getLock().isExpired()) {
removeExpired(tx, responsibleMap, resource);
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction lock for resource '" + resource.getResourcePath() + "' was already expired.");
} else {
if (lockInfo.isCommit()) {
TransactionListener[] txListeners;
synchronized (listeners) {
txListeners = listeners.values().toArray(new TransactionListener[listeners.values().size()]);
}
for (TransactionListener txListener : txListeners) {
txListener.beforeCommit(resource, lockToken);
}
DavException ex = null;
try {
tx.commit(resource);
} catch (DavException e) {
ex = e;
}
for (TransactionListener txListener : txListeners) {
txListener.afterCommit(resource, lockToken, ex == null);
}
if (ex != null) {
throw ex;
}
} else {
tx.rollback(resource);
}
removeReferences(tx, responsibleMap, resource);
}
}
/**
* Always returns null
*
* @param type
* @param scope
* @param resource
* @return null
* @see #getLock(Type, Scope, TransactionResource)
* @see LockManager#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope, org.apache.jackrabbit.webdav.DavResource)
*/
public ActiveLock getLock(Type type, Scope scope, DavResource resource) {
return null;
}
/**
* Returns true if the given lock token belongs to a lock that applies to
* the given resource, false otherwise. The token may either be retrieved
* from the {@link DavConstants#HEADER_LOCK_TOKEN Lock-Token header} or
* from the {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header}.
*
* @param token
* @param resource
* @return
* @see LockManager#hasLock(String token, DavResource resource)
*/
public boolean hasLock(String token, DavResource resource) {
return getLock(token, null, resource) != null;
}
/**
* Return the lock applied to the given resource or null
*
* @param type
* @param scope
* @param resource
* @return lock applied to the given resource or null
* @see LockManager#getLock(Type, Scope, DavResource)
* todo: is it correct to return one that specific lock, the current session is token-holder of?
*/
public ActiveLock getLock(Type type, Scope scope, TransactionResource resource) {
ActiveLock lock = null;
if (TransactionConstants.TRANSACTION.equals(type)) {
String[] sessionTokens = resource.getSession().getLockTokens();
int i = 0;
while (lock == null && i < sessionTokens.length) {
String lockToken = sessionTokens[i];
lock = getLock(lockToken, scope, resource);
i++;
}
}
return lock;
}
//-----------------------------< listener support >-------------------------
/**
* Adds a transaction listener to this TxLockManager
.
* @param listener the listener to add.
*/
public void addTransactionListener(TransactionListener listener) {
synchronized (listeners) {
listeners.put(listener, listener);
}
}
/**
* Removes a transaction listener from this TxLockManager
.
* @param listener the listener to remove.
*/
public void removeTransactionListener(TransactionListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
/**
* @param lockToken
* @param resource
* @return
*/
private ActiveLock getLock(String lockToken, Scope scope, DavResource resource) {
if (!(resource instanceof TransactionResource)) {
log.warn("TransactionResource expected");
return null;
}
ActiveLock lock = null;
Transaction tx = null;
TransactionMap m = map;
// check if main-map contains that txId
if (m.containsKey(lockToken)) {
tx = m.get(lockToken);
} else {
// look through all the nested tx-maps (i.e. global txs) for the given txId
Iterator it = m.values().iterator();
while (it.hasNext() && tx == null) {
Transaction txMap = it.next();
if (!txMap.isLocal()) {
m = (TransactionMap) txMap;
if (m.containsKey(lockToken)) {
tx = ((TransactionMap) txMap).get(lockToken);
}
}
}
}
if (tx != null) {
if (tx.getLock().isExpired()) {
removeExpired(tx, m, (TransactionResource) resource);
} else if (tx.appliesToResource(resource) && (scope == null || tx.getLock().getScope().equals(scope))) {
lock = tx.getLock();
}
}
return lock;
}
/**
* Return the map that may contain a transaction lock for the given resource.
* In case the resource provides a transactionId, the map must be a
* repository transaction that is identified by the given id and which in
* turn can act as map.
*
* @param resource
* @return responsible map.
* @throws DavException if no map could be retrieved.
*/
private TransactionMap getMap(TransactionResource resource)
throws DavException {
String txKey = resource.getTransactionId();
if (txKey == null) {
return map;
} else {
if (!map.containsKey(txKey)) {
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction map '" + map + " does not contain a transaction with TransactionId '" + txKey + "'.");
}
Transaction tx = map.get(txKey);
if (tx.isLocal()) {
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "TransactionId '" + txKey + "' points to a local transaction, that cannot act as transaction map");
} else if (tx.getLock() != null && tx.getLock().isExpired()) {
removeExpired(tx, map, resource);
throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to retrieve an expired global transaction.");
}
// tx is a global transaction that acts as map as well.
return (TransactionMap) tx;
}
}
/**
* Rollbacks the specified transaction and releases the lock. This includes
* the removal of all references.
*
* @param tx
* @param responsibleMap
* @param resource
*/
private static void removeExpired(Transaction tx, TransactionMap responsibleMap,
TransactionResource resource) {
log.debug("Removing expired transaction lock " + tx);
try {
tx.rollback(resource);
removeReferences(tx, responsibleMap, resource);
} catch (DavException e) {
log.error("Error while removing expired transaction lock: " + e.getMessage());
}
}
/**
* Create the required references to the new transaction specified by tx.
*
* @param tx
* @param responsibleMap
* @param resource
* @throws DavException
*/
private static void addReferences(Transaction tx, TransactionMap responsibleMap,
TransactionResource resource) {
log.debug("Adding transactionId '" + tx.getId() + "' as session lock token.");
resource.getSession().addLockToken(tx.getId());
responsibleMap.put(tx.getId(), tx);
resource.getSession().addReference(tx.getId());
}
/**
* Remove all references to the specified transaction.
*
* @param tx
* @param responsibleMap
* @param resource
*/
private static void removeReferences(Transaction tx, TransactionMap responsibleMap,
TransactionResource resource) {
log.debug("Removing transactionId '" + tx.getId() + "' from session lock tokens.");
resource.getSession().removeLockToken(tx.getId());
responsibleMap.remove(tx.getId());
resource.getSession().removeReference(tx.getId());
}
/**
* @param resource
* @return JCR session
*/
private static Session getRepositorySession(TransactionResource resource) throws DavException {
return JcrDavSession.getRepositorySession(resource.getSession());
}
//------------------------------------------< inner classes, interfaces >---
/**
* Internal Transaction
interface
*/
private interface Transaction {
TxActiveLock getLock();
/**
* @return the id of this transaction.
*/
String getId();
/**
* @return path of the lock holding resource
*/
String getResourcePath();
/**
* @param resource
* @return true if the lock defined by this transaction applies to the
* given resource, either due to the resource holding that lock or due
* to a deep lock hold by any ancestor resource.
*/
boolean appliesToResource(DavResource resource);
/**
* @return true if this transaction is used to allow for transient changes
* on the underlying repository, that may be persisted with the final
* UNLOCK request only.
*/
boolean isLocal();
/**
* Start this transaction.
*
* @param resource
* @throws DavException if an error occurs.
*/
void start(TransactionResource resource) throws DavException;
/**
* Commit this transaction
*
* @param resource
* @throws DavException if an error occurs.
*/
void commit(TransactionResource resource) throws DavException;
/**
* Rollback this transaction.
*
* @param resource
* @throws DavException if an error occurs.
*/
void rollback(TransactionResource resource) throws DavException;
}
/**
* Abstract transaction covering functionally to both implementations.
*/
private abstract static class AbstractTransaction extends TransactionMap implements Transaction {
private final DavResourceLocator locator;
private final TxActiveLock lock;
private AbstractTransaction(DavResourceLocator locator, TxActiveLock lock) {
this.locator = locator;
this.lock = lock;
}
//----------------------------------------------------< Transaction >---
/**
* @see #getLock()
*/
public TxActiveLock getLock() {
return lock;
}
/**
* @see #getId()
*/
public String getId() {
return lock.getToken();
}
/**
* @see #getResourcePath()
*/
public String getResourcePath() {
return locator.getResourcePath();
}
/**
* @see #appliesToResource(DavResource)
*/
public boolean appliesToResource(DavResource resource) {
if (locator.isSameWorkspace(resource.getLocator())) {
String lockResourcePath = getResourcePath();
String resPath = resource.getResourcePath();
while (!"".equals(resPath)) {
if (lockResourcePath.equals(resPath)) {
return true;
}
resPath = Text.getRelativeParent(resPath, 1);
}
}
return false;
}
}
/**
* Local transaction
*/
private final static class LocalTransaction extends AbstractTransaction {
private LocalTransaction(DavResourceLocator locator, TxActiveLock lock) {
super(locator, lock);
}
//----------------------------------------------------< Transaction >---
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#isLocal()
*/
public boolean isLocal() {
return true;
}
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#start(TransactionResource)
*/
public void start(TransactionResource resource) throws DavException {
try {
// make sure, the given resource represents an existing repository item
if (!getRepositorySession(resource).itemExists(resource.getLocator().getRepositoryPath())) {
throw new DavException(DavServletResponse.SC_CONFLICT, "Unable to start local transaction: no repository item present at " + getResourcePath());
}
} catch (RepositoryException e) {
log.error("Unexpected error: " + e.getMessage());
throw new JcrDavException(e);
}
}
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#commit(TransactionResource)
*/
public void commit(TransactionResource resource) throws DavException {
try {
getItem(resource).save();
} catch (RepositoryException e) {
throw new JcrDavException(e);
}
}
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#rollback(TransactionResource)
*/
public void rollback(TransactionResource resource) throws DavException {
try {
getItem(resource).refresh(false);
} catch (RepositoryException e) {
throw new JcrDavException(e);
}
}
//-------------------------------------------------< TransactionMap >---
/**
* Always throws DavException
.
*
* @see TransactionMap#putTransaction(String, org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction)
*/
@Override
public Transaction putTransaction(String key, Transaction value) throws DavException {
throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest a new transaction into a local one.");
}
//--------------------------------------------------------< private >---
/**
* Retrieve the repository item from the given transaction resource.
*
* @param resource
* @return
* @throws PathNotFoundException
* @throws RepositoryException
* @throws DavException
*/
private Item getItem(TransactionResource resource) throws PathNotFoundException, RepositoryException, DavException {
String itemPath = resource.getLocator().getRepositoryPath();
return getRepositorySession(resource).getItem(itemPath);
}
}
/**
* Global transaction
*/
private static class GlobalTransaction extends AbstractTransaction {
private Xid xid;
private GlobalTransaction(DavResourceLocator locator, TxActiveLock lock) {
super(locator, lock);
xid = new XidImpl(lock.getToken());
}
//----------------------------------------------------< Transaction >---
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#isLocal()
*/
public boolean isLocal() {
return false;
}
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#start(TransactionResource)
*/
public void start(TransactionResource resource) throws DavException {
XAResource xaRes = getXAResource(resource);
try {
xaRes.setTransactionTimeout((int) getLock().getTimeout() / 1000);
xaRes.start(xid, XAResource.TMNOFLAGS);
} catch (XAException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage());
}
}
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#commit(TransactionResource)
*/
public void commit(TransactionResource resource) throws DavException {
XAResource xaRes = getXAResource(resource);
try {
xaRes.commit(xid, false);
removeLocalTxReferences(resource);
} catch (XAException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage());
}
}
/**
* @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#rollback(TransactionResource)
*/
public void rollback(TransactionResource resource) throws DavException {
XAResource xaRes = getXAResource(resource);
try {
xaRes.rollback(xid);
removeLocalTxReferences(resource);
} catch (XAException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage());
}
}
//-------------------------------------------------< TransactionMap >---
@Override
public Transaction putTransaction(String key, Transaction value) throws DavException {
if (!(value instanceof LocalTransaction)) {
throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest global transaction into a global one.");
}
return super.put(key, value);
}
//--------------------------------------------------------< private >---
private XAResource getXAResource(TransactionResource resource) throws DavException {
/*
// commented, since server should be jackrabbit independent
Session session = resource.getSession().getRepositorySession();
if (session instanceof XASession) {
return ((XASession)session).getXAResource();
} else {
throw new DavException(DavServletResponse.SC_FORBIDDEN);
}
*/
throw new DavException(DavServletResponse.SC_FORBIDDEN);
}
private void removeLocalTxReferences(TransactionResource resource) {
for (Object o : values()) {
Transaction tx = (Transaction) o;
removeReferences(tx, this, resource);
}
}
}
/**
*
*/
private static class TransactionMap extends HashMap {
public Transaction get(String key) {
Transaction tx = null;
if (containsKey(key)) {
tx = super.get(key);
}
return tx;
}
public Transaction putTransaction(String key, Transaction value) throws DavException {
// any global and local transactions allowed.
return super.put(key, value);
}
}
/**
* Private class implementing Xid interface.
*/
private static class XidImpl implements Xid {
private final String id;
/**
* Create a new Xid
*
* @param id
*/
private XidImpl(String id) {
this.id = id;
}
/**
* @return 1
* @see javax.transaction.xa.Xid#getFormatId()
*/
public int getFormatId() {
// todo: define reasonable format id
return 1;
}
/**
* @return an empty byte array.
* @see javax.transaction.xa.Xid#getBranchQualifier()
*/
public byte[] getBranchQualifier() {
return new byte[0];
}
/**
* @return id as byte array
* @see javax.transaction.xa.Xid#getGlobalTransactionId()
*/
public byte[] getGlobalTransactionId() {
return id.getBytes();
}
}
}