/* * Copyright (C) 2006-2008 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * 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. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have recieved a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.filesys.repo; import java.io.Serializable; import java.util.Map; import javax.transaction.UserTransaction; import org.alfresco.filesys.state.FileState; import org.alfresco.filesys.state.FileStateTable; import org.alfresco.jlan.server.filesys.FileStatus; import org.alfresco.jlan.server.filesys.NotifyChange; import org.alfresco.jlan.smb.server.notify.NotifyChangeHandler; import org.alfresco.model.ContentModel; import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileFolderServiceType; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Node Monitor Class * *

Monitor node events from the node service to update the file state cache and feed notification events into * the file server change notification handler. * * @author gkspencer */ public class NodeMonitor extends TransactionListenerAdapter implements NodeServicePolicies.OnCreateNodePolicy, NodeServicePolicies.OnUpdatePropertiesPolicy, NodeServicePolicies.BeforeDeleteNodePolicy, NodeServicePolicies.OnMoveNodePolicy, Runnable { // Logging private static final Log logger = LogFactory.getLog(NodeMonitor.class); // Transaction object binding keys public static final String FileSysNodeEvent = "FileSysNodeEvent"; public static final String FileSysNodeEvent2 = "FileSysNodeEvent2"; // Services/components private PolicyComponent m_policyComponent; private NodeService m_nodeService; private FileFolderService m_fileFolderService; private PermissionService m_permissionService; private TransactionService m_transService; // Filesystem driver and context private ContentDiskDriver m_filesysDriver; private ContentContext m_filesysCtx; // File state table and change notification handler private FileStateTable m_stateTable; private NotifyChangeHandler m_changeHandler; // Root node path and store private String m_rootPath; private StoreRef m_storeRef; // Queue of node update events private NodeEventQueue m_eventQueue; // Thread for the main event processing private Thread m_thread; private boolean m_shutdown; /** * Class constructor * * @param filesysDriver ContentDiskDriver * @param filesysCtx ContentContext */ public NodeMonitor( ContentDiskDriver filesysDriver, ContentContext filesysCtx) { m_filesysDriver = filesysDriver; m_filesysCtx = filesysCtx; // Initialize the node monitor init(); } /** * Initialize the node monitor */ public final void init() { // Get various services via the filesystem driver m_nodeService = m_filesysDriver.getNodeService(); m_policyComponent = m_filesysDriver.getPolicyComponent(); m_fileFolderService = m_filesysDriver.getFileFolderService(); m_permissionService = m_filesysDriver.getPermissionService(); m_transService = m_filesysDriver.getTransactionService(); // Disable change notifications from the file server m_filesysCtx.setFileServerNotifications( false); // Register for node service events m_policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), this, new JavaBehaviour(this, "onCreateNode")); m_policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), this, new JavaBehaviour(this, "beforeDeleteNode")); m_policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteNode"), this, new JavaBehaviour(this, "onDeleteNode")); m_policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onMoveNode"), this, new JavaBehaviour(this, "onMoveNode")); m_policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), this, new JavaBehaviour(this, "onUpdateProperties")); // Get the store m_storeRef = m_filesysCtx.getRootNode().getStoreRef(); // Get the root node path String rootPath = (String) m_nodeService.getProperty( m_filesysCtx.getRootNode(), ContentModel.PROP_NAME); StringBuilder pathBuilder= new StringBuilder(); pathBuilder.append("/"); if ( rootPath != null && rootPath.length() > 0) pathBuilder.append( rootPath); m_rootPath = pathBuilder.toString(); // DEBUG if ( logger.isDebugEnabled()) logger.debug("Node monitor filesystem=" + m_filesysCtx.getDeviceName() + ", rootPath=" + m_rootPath); // Create the node event queue m_eventQueue = new NodeEventQueue(); // DEBUG if ( logger.isDebugEnabled()) logger.debug("Node monitor installed for " + m_filesysCtx.getDeviceName()); } /** * Start the node monitor thread */ public void startMonitor() { // Get the file state table and change notification handler, if enabled m_stateTable = m_filesysCtx.getStateTable(); m_changeHandler = m_filesysCtx.getChangeHandler(); // Start the event processing thread m_thread = new Thread( this); m_thread.setName( "NodeMonitor_" + m_filesysCtx.getDeviceName()); m_thread.setDaemon( true); m_thread.start(); // DEBUG if ( logger.isDebugEnabled()) logger.debug("NodeMonitor started, " + m_thread.getName()); } /** * Create node event * * @param childAssocRef ChildAssociationRef */ public void onCreateNode(ChildAssociationRef childAssocRef) { // Check if the node is a file/folder NodeRef nodeRef = childAssocRef.getChildRef(); if ( nodeRef.getStoreRef().equals( m_storeRef) == false) return; QName nodeType = m_nodeService.getType( nodeRef); FileFolderServiceType fType = m_fileFolderService.getType( nodeType); if ( fType != FileFolderServiceType.INVALID) { // DEBUG if ( logger.isDebugEnabled()) { // Get the full path to the file/folder node Path nodePath = m_nodeService.getPath( nodeRef); String fName = (String) m_nodeService.getProperty( nodeRef, ContentModel.PROP_NAME); logger.debug("OnCreateNode: nodeRef=" + nodeRef + ", name=" + fName + ", path=" + nodePath.toDisplayPath(m_nodeService, m_permissionService)); } // Create an event to process the node creation NodeEvent nodeEvent = new CreateNodeEvent( fType, nodeRef); // Store the event in the transaction until committed, and register the transaction listener AlfrescoTransactionSupport.bindListener( this); AlfrescoTransactionSupport.bindResource( FileSysNodeEvent, nodeEvent); } } /** * Update properties event * * @param nodeRef NodeRef * @param before Map * @param after Map */ public void onUpdateProperties( NodeRef nodeRef, Map before, Map after) { // Check that the node is in our store if ( nodeRef.getStoreRef().equals( m_storeRef) == false) return; // Check if the node is a file/folder QName nodeType = m_nodeService.getType( nodeRef); FileFolderServiceType fType = m_fileFolderService.getType( nodeType); if ( fType != FileFolderServiceType.INVALID) { // Check if there has been a lock change NodeEvent nodeEvent = null; String beforeLock = (String) before.get( ContentModel.PROP_LOCK_TYPE); String afterLock = (String) after.get( ContentModel.PROP_LOCK_TYPE); if (( beforeLock != null && afterLock == null) || ( beforeLock == null && afterLock != null)) { // Process the update nodeEvent = new LockNodeEvent( fType, nodeRef, beforeLock, afterLock); } // Check if a node event has been created if ( nodeEvent != null) { // Check for an existing event String eventKey = FileSysNodeEvent; if ( AlfrescoTransactionSupport.getResource( FileSysNodeEvent) != null) eventKey = FileSysNodeEvent2; // Store the event in the transaction until committed, and register the transaction listener AlfrescoTransactionSupport.bindListener( this); AlfrescoTransactionSupport.bindResource( eventKey, nodeEvent); } } } /** * Delete node event * * @param childAssocRef ChildAssociationRef * @param isArchiveNode boolean */ public void onDeleteNode(ChildAssociationRef childAssocRef, boolean isArchivedNode) { // Check if there is a node event stored in the transaction NodeEvent nodeEvent = (NodeEvent) AlfrescoTransactionSupport.getResource( FileSysNodeEvent); if ( nodeEvent != null && nodeEvent instanceof DeleteNodeEvent) { // Should be the same node id DeleteNodeEvent deleteEvent = (DeleteNodeEvent) nodeEvent; NodeRef nodeRef = childAssocRef.getChildRef(); if ( nodeRef.equals( deleteEvent.getNodeRef())) { // Confirm the node delete deleteEvent.setDeleteConfirm( true); // DEBUG if ( logger.isDebugEnabled()) logger.debug("OnDeleteNode: confirm delete nodeRef=" + nodeRef); } } } /** * Move node event * * @param oldChildAssocRef ChildAssociationRef * @param newChildAssocRef ChildAssociationRef */ public void onMoveNode(ChildAssociationRef oldChildAssocRef, ChildAssociationRef newChildAssocRef) { // Check if the node is a file/folder, and for our store NodeRef nodeRef = oldChildAssocRef.getChildRef(); if ( nodeRef.getStoreRef().equals( m_storeRef) == false) return; QName nodeType = m_nodeService.getType( nodeRef); FileFolderServiceType fType = m_fileFolderService.getType( nodeType); if ( fType != FileFolderServiceType.INVALID) { // Get the full path to the file/folder node Path nodePath = m_nodeService.getPath( nodeRef); String fName = (String) m_nodeService.getProperty( nodeRef, ContentModel.PROP_NAME); // Build the share relative path to the node StringBuilder pathStr = new StringBuilder(); pathStr.append( nodePath.toDisplayPath(m_nodeService, m_permissionService)); if ( pathStr.charAt(pathStr.length() - 1) != '/' && pathStr.charAt(pathStr.length() - 1) != '\\') pathStr.append("\\"); pathStr.append( fName); String relPath = pathStr.toString(); // DEBUG if ( logger.isDebugEnabled()) logger.debug("OnMoveNode: nodeRef=" + nodeRef + ", relPath=" + relPath); // Queue an event to process the node move if ( relPath.startsWith( m_rootPath)) { // Create a move event NodeEvent nodeEvent = new MoveNodeEvent( fType, nodeRef, relPath, newChildAssocRef.getChildRef()); // Store the event in the transaction until committed, and register the transaction listener AlfrescoTransactionSupport.bindListener( this); AlfrescoTransactionSupport.bindResource( FileSysNodeEvent, nodeEvent); } } } /** * Before delete node event * * @param nodeRef NodeRef */ public void beforeDeleteNode(NodeRef nodeRef) { // Check if the node is in the filesystem store if ( nodeRef.getStoreRef().equals( m_storeRef) == false) return; // Check if the node is a file/folder QName nodeType = m_nodeService.getType( nodeRef); FileFolderServiceType fType = m_fileFolderService.getType( nodeType); if ( fType != FileFolderServiceType.INVALID) { // Get the full path to the file/folder node Path nodePath = m_nodeService.getPath( nodeRef); String fName = (String) m_nodeService.getProperty( nodeRef, ContentModel.PROP_NAME); // Build the share relative path to the node StringBuilder pathStr = new StringBuilder(); pathStr.append( nodePath.toDisplayPath(m_nodeService, m_permissionService)); if ( pathStr.length() == 0 || (pathStr.charAt(pathStr.length() - 1) != '/' && pathStr.charAt(pathStr.length() - 1) != '\\')) pathStr.append("\\"); pathStr.append( fName); String relPath = pathStr.toString(); // Create an event to process the node deletion if ( relPath.startsWith( m_rootPath)) { // Create a delete event NodeEvent nodeEvent = new DeleteNodeEvent( fType, nodeRef, relPath); // Store the event in the transaction until committed, and register the transaction listener AlfrescoTransactionSupport.bindListener( this); AlfrescoTransactionSupport.bindResource( FileSysNodeEvent, nodeEvent); // DEBUG if ( logger.isDebugEnabled()) logger.debug("BeforeDeleteNode: nodeRef=" + nodeRef + ", relPath=" + relPath); } } } /** * Request the node monitor thread to shut down */ public final void shutdownRequest() { if ( m_thread != null) { // Set the shutdown request flag m_shutdown = true; // Interrupt the event processing thread try { m_thread.interrupt(); } catch ( Exception ex) { } } } /** * Transaction processing hook */ public void afterCommit() { // Get the node event that was stored in the transaction NodeEvent nodeEvent = (NodeEvent) AlfrescoTransactionSupport.getResource( FileSysNodeEvent); if ( nodeEvent != null) { // Queue the primary event for processing m_eventQueue.addEvent( nodeEvent); // Check for a secondary event nodeEvent = (NodeEvent) AlfrescoTransactionSupport.getResource(FileSysNodeEvent2); if ( nodeEvent != null) m_eventQueue.addEvent( nodeEvent); } } /** * Event queue processing */ public void run() { // Clear the shutdown flag m_shutdown = false; // Use the system user as the authenticated context for the node monitor AuthenticationComponent authComponent = m_filesysDriver.getAuthComponent(); authComponent.setCurrentUser( authComponent.getSystemUserName()); // Loop until shutdown NodeEvent nodeEvent = null; UserTransaction tx = null; while ( m_shutdown == false) { // Wait for an event to process try { nodeEvent = m_eventQueue.removeEvent(); } catch ( InterruptedException ex) { } // Check for a shutdown if ( m_shutdown == true) continue; // DEBUG if ( logger.isDebugEnabled()) logger.debug("Processing event " + nodeEvent); // Create a transaction tx = m_transService.getUserTransaction( true); try { // Start the transaction tx.begin(); // Process the event if ( nodeEvent instanceof CreateNodeEvent) { // Node created processCreateNode((CreateNodeEvent) nodeEvent); } else if ( nodeEvent instanceof DeleteNodeEvent) { // Node deleted processDeleteNode((DeleteNodeEvent) nodeEvent); } else if ( nodeEvent instanceof MoveNodeEvent) { // Node moved processMoveNode((MoveNodeEvent) nodeEvent); } else if ( nodeEvent instanceof LockNodeEvent) { // Node locked/unlocked processLockNode(( LockNodeEvent) nodeEvent); } // Commit the transaction tx.commit(); tx = null; } catch ( Exception ex) { logger.error( ex); } finally { // If there is an active transaction then roll it back if ( tx != null) { try { tx.rollback(); } catch (Exception ex) { logger.warn("Failed to rollback transaction", ex); } } } } } /** * Process a create node event * * @param createEvent CreateNodeEvent */ private final void processCreateNode(NodeEvent createEvent) { // Get the full path to the file/folder node Path nodePath = m_nodeService.getPath( createEvent.getNodeRef()); String relPath = nodePath.toDisplayPath(m_nodeService, m_permissionService); String fName = (String) m_nodeService.getProperty( createEvent.getNodeRef(), ContentModel.PROP_NAME); // Check if the path is within the filesystem view if ( relPath.startsWith( m_rootPath)) { // DEBUG if ( logger.isDebugEnabled()) logger.debug("CreateNode nodeRef=" + createEvent.getNodeRef() + ", fName=" + fName + ", path=" + relPath); // Build the full file path StringBuilder fullPath = new StringBuilder(); fullPath.append( relPath.substring( m_rootPath.length())); fullPath.append( "/"); fullPath.append( fName); relPath = fullPath.toString(); // Update an existing file state to indicate that the file exists, may have been marked as deleted if ( m_stateTable != null) { // Check if there is file state for this file FileState fState = m_stateTable.findFileState( relPath); if ( fState != null && fState.exists() == false) { // Check if the new node is a file or folder if ( createEvent.getFileType() == FileFolderServiceType.FILE) fState.setFileStatus(FileStatus.FileExists); else fState.setFileStatus(FileStatus.DirectoryExists); // DEBUG if ( logger.isDebugEnabled()) logger.debug("CreateNode updated file state - " + fState); } } // If change notifications are enabled then send an event to registered listeners if ( m_changeHandler != null) { // Check if there are any active notifications if ( m_changeHandler.getGlobalNotifyMask() != 0) { // Send a file created event to the change notification handler if ( createEvent.getFileType() == FileFolderServiceType.FILE) m_changeHandler.notifyFileChanged(NotifyChange.ActionAdded, relPath); else m_changeHandler.notifyDirectoryChanged(NotifyChange.ActionAdded, relPath); // DEBUG if ( logger.isDebugEnabled()) logger.debug("CreateNode queued change notification"); } } } else { // DEBUG if ( logger.isDebugEnabled()) logger.debug("CreateNode ignored nodeRef=" + createEvent.getNodeRef() + ", path=" + relPath); } } /** * Process a node delete event * * @param deleteEvent DeleteNodeEvent */ private final void processDeleteNode(DeleteNodeEvent deleteEvent) { // Check if the delete was confirmed if ( deleteEvent.hasDeleteConfirm() == false) { // DEBUG if ( logger.isDebugEnabled()) logger.debug("DeleteNode not confirmed, nodeRef=" + deleteEvent.getNodeRef() + ", path=" + deleteEvent.getPath()); return; } // Strip the root path String relPath = deleteEvent.getPath().substring( m_rootPath.length()).replace( '/', '\\'); // DEBUG if ( logger.isDebugEnabled()) logger.debug("DeleteNode nodeRef=" + deleteEvent.getNodeRef() + ", path=" + relPath); // Update an existing file state to indicate that the file does not exist if ( m_stateTable != null) { // Check if there is file state for this file FileState fState = m_stateTable.findFileState( relPath); if ( fState != null && fState.exists() == true) { // Mark the file/folder as no longer existing fState.setFileStatus(FileStatus.NotExist); // DEBUG if ( logger.isDebugEnabled()) logger.debug("DeleteNode updated file state - " + fState); } } // If change notifications are enabled then send an event to registered listeners if ( m_changeHandler != null) { // Check if there are any active notifications if ( m_changeHandler.getGlobalNotifyMask() != 0) { // Send a file created event to the change notification handler if ( deleteEvent.getFileType() == FileFolderServiceType.FILE) m_changeHandler.notifyFileChanged(NotifyChange.ActionRemoved, relPath); else m_changeHandler.notifyDirectoryChanged(NotifyChange.ActionRemoved, relPath); // DEBUG if ( logger.isDebugEnabled()) logger.debug("DeleteNode queued change notification"); } } } /** * Process a node move event * * @param moveEvent MoveNodeEvent */ private final void processMoveNode(MoveNodeEvent moveEvent) { // Strip the root path String fromPath = moveEvent.getPath().substring( m_rootPath.length()).replace( '/', '\\'); // Get the destination relative path Path nodePath = m_nodeService.getPath( moveEvent.getMoveToNodeRef()); String fName = (String) m_nodeService.getProperty( moveEvent.getMoveToNodeRef(), ContentModel.PROP_NAME); // Build the share relative path to the destination StringBuilder pathStr = new StringBuilder(); pathStr.append( nodePath.toDisplayPath(m_nodeService, m_permissionService)); if ( pathStr.charAt(pathStr.length() - 1) != '/' && pathStr.charAt(pathStr.length() - 1) != '\\') pathStr.append("\\"); pathStr.append( fName); String toPath = pathStr.toString().substring( m_rootPath.length()).replace( '/', '\\'); // DEBUG if ( logger.isDebugEnabled()) logger.debug("MoveNode fromPath=" + fromPath + ", toPath=" + toPath); // Update an existing file state to indicate that the file does not exist if ( m_stateTable != null) { // Check if there is file state for the orginal file/folder FileState fState = m_stateTable.findFileState( fromPath); if ( fState != null && fState.exists() == true) { // Mark the file/folder as no longer existing fState.setFileStatus(FileStatus.NotExist); // DEBUG if ( logger.isDebugEnabled()) logger.debug("MoveNode updated state for fromPath=" + fromPath); } // Check if there is a file state for the destination file/folder fState = m_stateTable.findFileState( toPath); if ( fState != null && fState.exists() == false) { // Indicate the the file or folder exists if ( moveEvent.getFileType() == FileFolderServiceType.FILE) fState.setFileStatus(FileStatus.FileExists); else fState.setFileStatus(FileStatus.DirectoryExists); // DEBUG if ( logger.isDebugEnabled()) logger.debug("MoveNode updated state for toPath=" + toPath); } } // If change notifications are enabled then send an event to registered listeners if ( m_changeHandler != null) { // Check if there are any active notifications if ( m_changeHandler.getGlobalNotifyMask() != 0) { // Send a file renamed event to the change notification handler m_changeHandler.notifyRename( fromPath, toPath); // DEBUG if ( logger.isDebugEnabled()) logger.debug("MoveNode queued change notification"); } } } /** * Process a node lock/unlock event * * @param lockEvent LockNodeEvent */ private final void processLockNode(LockNodeEvent lockEvent) { // Get the full path to the file/folder node Path nodePath = m_nodeService.getPath( lockEvent.getNodeRef()); String relPath = nodePath.toDisplayPath(m_nodeService, m_permissionService); String fName = (String) m_nodeService.getProperty( lockEvent.getNodeRef(), ContentModel.PROP_NAME); // Check if the path is within the filesystem view if ( relPath.startsWith( m_rootPath)) { // DEBUG if ( logger.isDebugEnabled()) logger.debug("LockNode nodeRef=" + lockEvent.getNodeRef() + ", fName=" + fName + ", path=" + relPath); // Build the full file path StringBuilder fullPath = new StringBuilder(); fullPath.append( relPath.substring( m_rootPath.length())); fullPath.append( "/"); fullPath.append( fName); relPath = fullPath.toString().replace( '/', '\\'); // Node has been locked or unlocked, send a change notification to indicate the file attributes have changed if ( m_changeHandler != null) { // Send out a change of attributes notification m_changeHandler.notifyAttributesChanged( relPath, lockEvent.getFileType() == FileFolderServiceType.FILE ? false : true); // DEBUG if ( logger.isDebugEnabled()) logger.debug("LockNode queued change notification"); } } } }