/* * 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; /** * <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 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<TransactionListener, TransactionListener> listeners = new IdentityHashMap<TransactionListener, TransactionListener>(); /** * 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 identified 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 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 <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().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 <code>TxLockManager</code>. * @param listener the listener to add. */ public void addTransactionListener(TransactionListener listener) { synchronized (listeners) { listeners.put(listener, listener); } } /** * Removes a transaction listener from this <code>TxLockManager</code>. * @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<Transaction> 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 <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; } //----------------------------------------------------< 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 <code>DavException</code>. * * @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<String, Transaction> { 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(); } } }