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