/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * * Copyright 2006 - 2013 Pentaho Corporation. All rights reserved. */ package org.pentaho.platform.repository2.unified.jcr; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.platform.api.mt.ITenantedPrincipleNameResolver; import org.pentaho.platform.engine.core.system.PentahoSessionHolder; import org.pentaho.platform.repository.RepositoryFilenameUtils; import org.pentaho.platform.repository2.unified.ServerRepositoryPaths; import org.springframework.util.Assert; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.lock.Lock; import javax.jcr.lock.LockManager; import javax.jcr.security.AccessControlManager; import javax.jcr.security.Privilege; import java.io.Serializable; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; /** * Default implementation of {@link ILockHelper}. If user {@code suzy} in tenant {@code acme} locks a file with * UUID {@code abc} then this implementation will store the lock token {@code xyz} as * {@code /pentaho/acme/home/suzy/.lockTokens/abc/xyz}. It is assumed that {@code /pentaho/acme/home/suzy} is never * versioned! Putting lock token storage beneath the user's home folder provides access control. * * <p> * This implementation stores a lock owner, lock date, and lock message in the ownerInfo payload. See JCR 2.0 * section 17.3. If implemented as custom properties, then a versioned node would require a checkout and checkin to * lock a file. There is one caveat: implementations of JCR are free to ignore the ownerInfo payload. In that case, * the implementation sets the value. If that happens, we simply return that value as the lock owner and date and * message are null. * </p> * * @author mlowery */ public class DefaultLockHelper implements ILockHelper { // ~ Static fields/initializers // ====================================================================================== private static final String FOLDER_NAME_LOCK_TOKENS = ".lockTokens"; //$NON-NLS-1$ private static final char LOCK_OWNER_INFO_SEPARATOR = ':'; private static final String LOCK_OWNER_INFO_SEPARATOR_REGEX = "\\" + LOCK_OWNER_INFO_SEPARATOR; //$NON-NLS-1$ private static final List<Character> RESERVED_CHARS = Arrays.asList( new Character[] { LOCK_OWNER_INFO_SEPARATOR } ); private static final Log logger = LogFactory.getLog( DefaultLockHelper.class ); private static final int POSITION_LOCK_OWNER = 0; private static final int POSITION_LOCK_DATE = 1; private static final int POSITION_LOCK_MESSAGE = 2; ITenantedPrincipleNameResolver userNameUtils; // ~ Instance fields // ================================================================================================= // ~ Constructors // ==================================================================================================== public DefaultLockHelper( ITenantedPrincipleNameResolver userNameUtils ) { super(); this.userNameUtils = userNameUtils; } // ~ Methods // ========================================================================================================= /** * Stores a lock token associated with the session's user. */ protected void addLockToken( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { Node lockTokensNode = getOrCreateLockTokensNode( session, pentahoJcrConstants, lock ); Node newLockTokenNode = lockTokensNode.addNode( lock.getNode().getIdentifier(), pentahoJcrConstants.getPHO_NT_LOCKTOKENSTORAGE() ); newLockTokenNode.setProperty( pentahoJcrConstants.getPHO_LOCKEDNODEREF(), lock.getNode() ); newLockTokenNode.setProperty( pentahoJcrConstants.getPHO_LOCKTOKEN(), lock.getLockToken() ); session.save(); } /** * Returns all lock tokens belonging to the session's user. Lock tokens can then be added to the session by * calling {@code Session.addLockToken(token)}. * * <p> * Callers should call {#link {@link #canUnlock(Session, PentahoJcrConstants, Lock)} if the token is being * retrieved for the purpose of an unlock. * </p> */ protected String getLockToken( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { Node lockTokensNode = getOrCreateLockTokensNode( session, pentahoJcrConstants, lock ); NodeIterator nodes = lockTokensNode.getNodes( lock.getNode().getIdentifier() ); Assert.isTrue( nodes.hasNext() ); return nodes.nextNode().getProperty( pentahoJcrConstants.getPHO_LOCKTOKEN() ).getString(); } /** * Removes a lock token so that it can never be associated with anyone's session again. (To be called after the * file has been unlocked and therefore the token associated with the lock is unnecessary.) */ public void removeLockToken( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { Node lockTokensNode = getOrCreateLockTokensNode( session, pentahoJcrConstants, lock ); NodeIterator nodes = lockTokensNode.getNodes( lock.getNode().getIdentifier() ); if ( nodes.hasNext() ) { nodes.nextNode().remove(); } session.save(); } protected Node getOrCreateLockTokensNode( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { String absPath = ServerRepositoryPaths.getUserHomeFolderPath( userNameUtils.getTenant( getLockOwner( session, pentahoJcrConstants, lock ) ), userNameUtils.getPrincipleName( getLockOwner( session, pentahoJcrConstants, lock ) ) ); Node userHomeFolderNode = (Node) session.getItem( absPath ); if ( userHomeFolderNode.hasNode( FOLDER_NAME_LOCK_TOKENS ) ) { return userHomeFolderNode.getNode( FOLDER_NAME_LOCK_TOKENS ); } else { Node lockTokensNode = userHomeFolderNode.addNode( FOLDER_NAME_LOCK_TOKENS, pentahoJcrConstants.getPHO_NT_INTERNALFOLDER() ); session.save(); return lockTokensNode; } } /** * {@inheritDoc} */ public boolean canUnlock( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { String absPath = ServerRepositoryPaths.getUserHomeFolderPath( userNameUtils.getTenant( getLockOwner( session, pentahoJcrConstants, lock ) ), userNameUtils.getPrincipleName( getLockOwner( session, pentahoJcrConstants, lock ) ) ); AccessControlManager acMgr = session.getAccessControlManager(); return acMgr.hasPrivileges( absPath, new Privilege[] { acMgr.privilegeFromName( "jcr:read" ), acMgr.privilegeFromName( "jcr:write" ), //$NON-NLS-1$ //$NON-NLS-2$ acMgr.privilegeFromName( "jcr:lockManagement" ) } ); //$NON-NLS-1$ } /** * {@inheritDoc} */ public void addLockTokenToSessionIfNecessary( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Serializable fileId ) throws RepositoryException { Node fileNode = session.getNodeByIdentifier( fileId.toString() ); if ( fileNode.isLocked() ) { LockManager lockManager = session.getWorkspace().getLockManager(); Lock lock = lockManager.getLock( fileNode.getPath() ); String lockToken = getLockToken( session, pentahoJcrConstants, lock ); lockManager.addLockToken( lockToken ); } } /** * {@inheritDoc} */ public void removeLockTokenFromSessionIfNecessary( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Serializable fileId ) throws RepositoryException { Node fileNode = session.getNodeByIdentifier( fileId.toString() ); if ( fileNode.isLocked() ) { LockManager lockManager = session.getWorkspace().getLockManager(); Lock lock = lockManager.getLock( fileNode.getPath() ); String lockToken = getLockToken( session, pentahoJcrConstants, lock ); lockManager.removeLockToken( lockToken ); } } /** * {@inheritDoc} */ public void unlockFile( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Serializable fileId ) throws RepositoryException { Node fileNode = session.getNodeByIdentifier( fileId.toString() ); LockManager lockManager = session.getWorkspace().getLockManager(); Lock lock = lockManager.getLock( fileNode.getPath() ); String lockToken = getLockToken( session, pentahoJcrConstants, lock ); lockManager.addLockToken( lockToken ); // get the lock again so that it has a non-null lockToken lock = lockManager.getLock( fileNode.getPath() ); // don't need lock token anymore removeLockToken( session, pentahoJcrConstants, lock ); lockManager.unlock( fileNode.getPath() ); } /** * {@inheritDoc} */ public void lockFile( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Serializable fileId, final String message ) throws RepositoryException { LockManager lockManager = session.getWorkspace().getLockManager(); // locks are always deep in this impl final boolean isDeep = true; // locks are always open-scoped since a session is short-lived and all work occurs in a transaction // anyway; from spec, "if a lock is enabled and then disabled within the same transaction, its effect never // makes it to the persistent workspace and therefore it does nothing" final boolean isSessionScoped = false; final long timeoutHint = Long.MAX_VALUE; final String ownerInfo = makeOwnerInfo( JcrTenantUtils.getTenantedUser( PentahoSessionHolder.getSession().getName() ), Calendar .getInstance().getTime(), message ); Node fileNode = session.getNodeByIdentifier( fileId.toString() ); Assert.isTrue( fileNode.isNodeType( pentahoJcrConstants.getMIX_LOCKABLE() ) ); Lock lock = lockManager.lock( fileNode.getPath(), isDeep, isSessionScoped, timeoutHint, ownerInfo ); addLockToken( session, pentahoJcrConstants, lock ); } private String makeOwnerInfo( final String lockOwner, final Date lockDate, final String lockMessage ) { return escape( lockOwner ) + LOCK_OWNER_INFO_SEPARATOR + lockDate.getTime() + LOCK_OWNER_INFO_SEPARATOR + escape( lockMessage ); } @Override public Date getLockDate( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { String[] tokens = tokenize( lock.getLockOwner() ); if ( tokens != null ) { long date; try { date = Long.parseLong( tokens[POSITION_LOCK_DATE] ); return new Date( date ); } catch ( NumberFormatException e ) { logger.debug( "could not parse lock date; returning null", e ); //$NON-NLS-1$ } } return null; } @Override public String getLockMessage( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { String[] tokens = tokenize( lock.getLockOwner() ); if ( tokens != null ) { return unescape( tokens[POSITION_LOCK_MESSAGE] ); } return null; } @Override public String getLockOwner( final Session session, final PentahoJcrConstants pentahoJcrConstants, final Lock lock ) throws RepositoryException { String[] tokens = tokenize( lock.getLockOwner() ); if ( tokens != null ) { return unescape( tokens[POSITION_LOCK_OWNER] ); } // return whatever the implementation stored in this property return lock.getLockOwner(); } private String[] tokenize( final String ownerInfo ) { if ( ownerInfo != null ) { String[] tokens = ownerInfo.split( LOCK_OWNER_INFO_SEPARATOR_REGEX ); if ( tokens.length == 3 ) { return tokens; } } return null; } private static String escape( final String in ) { if ( in == null || in.trim().equals( "" ) ) { //$NON-NLS-1$ return ""; //$NON-NLS-1$ } return RepositoryFilenameUtils.escape( in, RESERVED_CHARS ); } private static String unescape( final String in ) { if ( in == null || in.trim().equals( "" ) ) { //$NON-NLS-1$ return ""; //$NON-NLS-1$ } return RepositoryFilenameUtils.unescape( in ); } // public static void main(final String[] args) { // System.out.println("'" + escape(null) + "'"); // System.out.println("'" + escape("") + "'"); // System.out.println("'" + escape("hello") + "'"); // System.out.println("'" + escape("hell:o") + "'"); // System.out.println("'" + escape("hello:") + "'"); // System.out.println("'" + escape(":hello") + "'"); // System.out.println("'" + escape("hell::o") + "'"); // System.out.println("'" + escape("hell\\::o") + "'"); // // System.out.println("'" + unescape(null) + "'"); // System.out.println("'" + unescape("") + "'"); // System.out.println("'" + unescape("hello") + "'"); // System.out.println("'" + unescape("hell\\:o") + "'"); // System.out.println("'" + unescape("hello\\:") + "'"); // System.out.println("'" + unescape("\\:hello") + "'"); // System.out.println("'" + unescape("hell\\:\\:o") + "'"); // System.out.println("'" + unescape("hell\\\\:\\:o") + "'"); // // System.out.println(Arrays.toString("su%3Azy:1332272120111:lock within versioned folder" // .split(LOCK_OWNER_INFO_SEPARATOR_REGEX))); // } }