| /* |
| * 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(); |
| } |
| } |
| } |