blob: fe27110ac0876ac3855322f92f03ed0c02a84606 [file] [log] [blame]
/*
* Copyright 2004-2005 The Apache Software Foundation or its licensors,
* as applicable.
*
* Licensed 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.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.DavSession;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.apache.jackrabbit.webdav.jcr.JcrDavException;
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.apache.jackrabbit.util.Text;
import org.apache.log4j.Logger;
import javax.jcr.RepositoryException;
import javax.jcr.Item;
import javax.jcr.PathNotFoundException;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.util.HashMap;
import java.util.Iterator;
/**
* <code>TxLockManagerImpl</code> manages locks with locktype
* '{@link TransactionConstants#TRANSACTION dcr:transaction}'.
* <p/>
* 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 explicitely 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 = Logger.getLogger(TxLockManagerImpl.class);
private TransactionMap map = new TransactionMap();
/**
* 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 <code>null</code> 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 indentified by the given lock token.
*
* @param lockInfo
* @param lockToken
* @param resource
* @return the lock
* @throws DavException
* @throws IllegalArgumentException if the resource is <code>null</code> 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 lockdid 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()) {
tx.commit(resource);
} 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;
}
/**
* Return the lock applied to the given resource or <code>null</code>
*
* @param type
* @param scope
* @param resource
* @return lock applied to the given resource or <code>null</code>
* @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().getRepositorySession().getLockTokens();
int i = 0;
while (lock == null && i < sessionTokens.length) {
String lockToken = sessionTokens[i];
lock = getLock(lockToken, scope, resource);
i++;
}
}
return lock;
}
/**
* @param lockToken
* @param resource
* @return
*/
private ActiveLock getLock(String lockToken, Scope scope, DavResource resource) {
if (!(resource instanceof TransactionResource)) {
log.info("");
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 = (Transaction) 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;
}
/**
* 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 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.info("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) throws DavException {
log.info("Adding transactionId '" + tx.getId() + "' as session lock token.");
resource.getSession().getRepositorySession().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.info("Removing transactionId '" + tx.getId() + "' from session lock tokens.");
resource.getSession().getRepositorySession().removeLockToken(tx.getId());
responsibleMap.remove(tx.getId());
resource.getSession().removeReference(tx.getId());
}
//------------------------------------------< inner classes, interfaces >---
/**
* Internal <code>Transaction</code> 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;
}
/**
* @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;
}
}
/**
*
*/
private final static class LocalTransaction extends AbstractTransaction {
private LocalTransaction(DavResourceLocator locator, TxActiveLock lock) {
super(locator, lock);
}
public boolean isLocal() {
return true;
}
public void start(TransactionResource resource) throws DavException {
try {
// make sure, the given resource represents an existing repository item
if (!resource.getSession().getRepositorySession().itemExists(getResourcePath())) {
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);
}
}
public void commit(TransactionResource resource) throws DavException {
try {
getItem(resource).save();
} catch (RepositoryException e) {
throw new JcrDavException(e);
}
}
public void rollback(TransactionResource resource) throws DavException {
try {
getItem(resource).refresh(false);
} catch (RepositoryException e) {
throw new JcrDavException(e);
}
}
private Item getItem(TransactionResource resource) throws PathNotFoundException, RepositoryException {
DavSession session = resource.getSession();
String itemPath = resource.getLocator().getJcrPath();
return session.getRepositorySession().getItem(itemPath);
}
public Transaction put(String key, Transaction value) throws DavException {
throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest a new transaction into a local one.");
}
}
/**
*
*/
private static class GlobalTransaction extends AbstractTransaction {
private Xid xid;
private GlobalTransaction(DavResourceLocator locator, TxActiveLock lock) {
super(locator, lock);
xid = new XidImpl(lock.getToken());
}
public boolean isLocal() {
return false;
}
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());
}
}
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());
}
}
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());
}
}
private XAResource getXAResource(TransactionResource resource) throws DavException {
/*
currently commented, since server should be jackrabbit independant
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) {
Iterator it = values().iterator();
while (it.hasNext()) {
Transaction tx = (Transaction) it.next();
removeReferences(tx, this, resource);
}
}
public Transaction put(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 (Transaction) super.put(key, value);
}
}
/**
*
*/
private static class TransactionMap extends HashMap {
public Transaction get(String key) {
Transaction tx = null;
if (containsKey(key)) {
tx = (Transaction) super.get(key);
}
return tx;
}
public Transaction put(String key, Transaction value) throws DavException {
// any global an local transactions allowed.
return (Transaction) 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();
}
}
}