diff --git a/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java b/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java new file mode 100644 index 0000000000..c9b492c002 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2006-2009 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.util.List; + +import org.alfresco.filesys.state.FileState; +import org.alfresco.filesys.state.FileStateTable; +import org.alfresco.jlan.server.filesys.FileInfo; +import org.alfresco.jlan.server.filesys.pseudo.PseudoFileList; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Cache Lookup Search Context Class + * + *

Use the file state cache to check for current timestamp values for file information being returned in + * the current search. + * + * @author gkspencer + */ +public class CacheLookupSearchContext extends ContentSearchContext { + + // Debug logging + + private static final Log logger = LogFactory.getLog(CacheLookupSearchContext.class); + + // File state cache + + private FileStateTable m_stateCache; + + // File information for the '.' and '..' pseduo entries, returned during a wildcard search + + private FileInfo m_dotInfo; + private FileInfo m_dotDotInfo; + + /** + * Class constructor + * + * @param cifsHelper Filesystem helper class + * @param results List of file/folder nodes that match the search pattern + * @param searchStr Search path + * @param pseudoList List of pseudo files to be blended into the returned list of files + * @param relPath Relative path being searched + * @param stateCache File state cache + */ + protected CacheLookupSearchContext( + CifsHelper cifsHelper, + List results, + String searchStr, + PseudoFileList pseudoList, + String relPath, + FileStateTable stateCache) + { + super(cifsHelper, results, searchStr, pseudoList, relPath); + super.setSearchString(searchStr); + + m_stateCache = stateCache; + } + + /** + * Return file information for the next file in the active search. Returns false if the search + * is complete. + * + * @param info FileInfo to return the file information. + * @return true if the file information is valid, else false + */ + public boolean nextFileInfo(FileInfo info) + { + // Get the file information for the next file + + if ( super.nextFileInfo( info) == false) + return false; + else if ( returningPseudoFiles() == true || m_stateCache == null) + return true; + + // We have a real file entry, check if there is a cache entry + + StringBuilder relPath = new StringBuilder( getRelativePath()); + relPath.append( info.getFileName()); + + FileState fstate = m_stateCache.findFileState( relPath.toString()); + + if ( fstate != null) + { + // Check if there are current timestamps that can be used to override the database values + + if ( fstate.hasAccessDateTime()) + info.setAccessDateTime( fstate.getAccessDateTime()); + + if ( fstate.hasModifyDateTime()) + info.setModifyDateTime( fstate.getModifyDateTime()); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Search timestamps from cache, path=" + info.getFileName()); + } + + // Indicate that the file information is valid + + return true; + } + + /** + * Check if the '.' and '..' pseudo file entries are available + * + * @return boolean + */ + public boolean hasDotFiles() { + return (m_dotInfo != null && m_dotDotInfo != null) ? true : false; + } + + /** + * Return the '.' pseudo file entry details + * + * @param finfo FileInfo + * @return boolean + */ + public boolean getDotInfo(FileInfo finfo) { + + // Check if the '.' file information is valid + + if ( m_dotInfo != null) { + finfo.copyFrom( m_dotInfo); + return true; + } + + // File information not valid + + return false; + } + + /** + * Return the '..' pseudo file entry details + * + * @param finfo FileInfo + * @return boolean + */ + public boolean getDotDotInfo(FileInfo finfo) { + + // Check if the '..' file information is valid + + if ( m_dotDotInfo != null) { + finfo.copyFrom( m_dotDotInfo); + return true; + } + + // File information not valid + + return false; + } + + /** + * Set the '.' pseudo file entry details + * + * @param finfo FileInfo + */ + protected void setDotInfo(FileInfo finfo) { + m_dotInfo = finfo; + if ( m_dotInfo != null) + m_dotInfo.setFileName( "."); + } + + /** + * Set the '..' pseudo file entry details + * + * @param finfo FileInfo + */ + protected void setDotDotInfo(FileInfo finfo) { + m_dotDotInfo = finfo; + if ( m_dotDotInfo != null) + m_dotDotInfo.setFileName( "."); + } + + /** + * Return the search as a string + * + * @return String + */ + public String toString() + { + StringBuilder sb = new StringBuilder(60); + + sb.append("[CacheLookupSearchContext searchStr="); + sb.append(getSearchString()); + sb.append(", resultCount="); + sb.append(getResultsSize()); + sb.append(", pseudoList="); + if ( getPseudoListSize() != 0) + sb.append( getPseudoListSize()); + else + sb.append("NULL"); + sb.append(",cache="); + sb.append(m_stateCache); + + if ( m_dotInfo != null) + sb.append(",Dot"); + if ( m_dotDotInfo != null) + sb.append(",DotDot"); + sb.append("]"); + + return sb.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 6d5338ed83..d49c6b462f 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -875,7 +875,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Access the device context ContentContext ctx = (ContentContext) tree.getContext(); - + try { String searchFileSpec = searchPath; @@ -1027,15 +1027,107 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa } } - // Build the search context to store the results + // Build the search context to store the results, use the cache lookup search for wildcard searches - SearchContext searchCtx = new ContentSearchContext(cifsHelper, results, searchFileSpec, pseudoList, paths[0]); + SearchContext searchCtx = null; + + if ( searchFileSpec.equals( "*")) + { + // Use a cache lookup search context + + CacheLookupSearchContext cacheContext = new CacheLookupSearchContext(cifsHelper, results, searchFileSpec, pseudoList, paths[0], ctx.getStateTable()); + searchCtx = cacheContext; + + // Set the '.' and '..' pseudo file entry details + + if ( searchFolderState != null && searchFolderState.hasNodeRef()) + { + // Get the '.' pseudo entry file details + + FileInfo finfo = cifsHelper.getFileInformation( searchFolderState.getNodeRef()); + + // Blend in any cached timestamps + + if ( searchFolderState != null) { + if ( searchFolderState.hasAccessDateTime()) + finfo.setAccessDateTime( searchFolderState.getAccessDateTime()); + + if ( searchFolderState.hasChangeDateTime()) + finfo.setChangeDateTime( searchFolderState.getChangeDateTime()); + + if ( searchFolderState.hasModifyDateTime()) + finfo.setModifyDateTime( searchFolderState.getModifyDateTime()); + } + + // Set the '.' pseudo entry details + + cacheContext.setDotInfo( finfo); + + // Check if the search folder has a parent, if we are at the root of the filesystem then re-use + // the file information + + if ( searchFolderState.getPath().equals( FileName.DOS_SEPERATOR_STR)) { + + // Searching the root folder, re-use the search folder file information for the '..' pseudo entry + + cacheContext.setDotDotInfo( finfo); + } + else { + + // Get the parent folder path + + String parentPath = searchFolderState.getPath(); + if ( parentPath.endsWith( FileName.DOS_SEPERATOR_STR) && parentPath.length() > 1) + parentPath = parentPath.substring(0, parentPath.length() - 1); + + int pos = parentPath.lastIndexOf( FileName.DOS_SEPERATOR_STR); + if ( pos != -1) + parentPath = parentPath.substring(0, pos + 1); + + // Get the file state for the parent path, if available + + FileState parentState = ctx.getStateTable().findFileState( parentPath); + NodeRef parentNode = null; + + if ( parentState != null) + parentNode = parentState.getNodeRef(); + + if ( parentState == null || parentNode == null) + parentNode = getNodeForPath( tree, parentPath); + + // Get the file information for the parent folder + + finfo = cifsHelper.getFileInformation( parentNode); + + // Blend in any cached timestamps + + if ( parentState != null) { + if ( parentState.hasAccessDateTime()) + finfo.setAccessDateTime( parentState.getAccessDateTime()); + + if ( parentState.hasChangeDateTime()) + finfo.setChangeDateTime( parentState.getChangeDateTime()); + + if ( parentState.hasModifyDateTime()) + finfo.setModifyDateTime( parentState.getModifyDateTime()); + } + + // Set the '..' pseudo entry details + + cacheContext.setDotDotInfo( finfo); + } + } + } + else + searchCtx = new ContentSearchContext(cifsHelper, results, searchFileSpec, pseudoList, paths[0]); // Debug if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_SEARCH)) - logger.debug("Started search: search path=" + searchPath + " attributes=" + attributes); - + logger.debug("Started search: search path=" + searchPath + " attributes=" + attributes + ", ctx=" + searchCtx); + + // Return the search context + return searchCtx; } catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) @@ -1618,7 +1710,9 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Get the device root NodeRef deviceRootNodeRef = ctx.getRootNode(); + String path = params.getPath(); + FileState parentState = null; // If the state table is available then try to find the parent folder node for the new file // to save having to walk the path @@ -1644,6 +1738,12 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) logger.debug("Create file using cached noderef for path " + paths[0]); } + + // Get the file state for the parent folder + + parentState = getStateForPath(tree, paths[0]); + if ( parentState == null && ctx.hasStateTable()) + parentState = ctx.getStateTable().findFileState( paths[0], true, true); } } @@ -1695,12 +1795,19 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) logger.debug("Create file, state=" + fstate); } + + // Update the parent folder file state + + if ( parentState != null) + parentState.updateModifyDateTime(); } // Debug if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) logger.debug("Created file: path=" + path + " file open parameters=" + params + " node=" + nodeRef + " network file=" + netFile); + + // Return the new network file return netFile; } @@ -1762,6 +1869,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa NodeRef deviceRootNodeRef = ctx.getRootNode(); String path = params.getPath(); + FileState parentState = null; // If the state table is available then try to find the parent folder node for the new folder // to save having to walk the path @@ -1787,6 +1895,12 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) logger.debug("Create file using cached noderef for path " + paths[0]); } + + // Get the file state for the parent folder + + parentState = getStateForPath(tree, paths[0]); + if ( parentState == null && ctx.hasStateTable()) + parentState = ctx.getStateTable().findFileState( paths[0], true, true); } } @@ -1809,8 +1923,13 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // DEBUG if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) - logger.debug("Creaste folder, state=" + fstate); + logger.debug("Create folder, state=" + fstate); } + + // Update the parent folder file state + + if ( parentState != null) + parentState.updateModifyDateTime(); } // Debug @@ -1879,7 +1998,27 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Remove the file state if ( ctx.hasStateTable()) + { + // Remove the file state + ctx.getStateTable().removeFileState(dir); + + // Update, or create, a parent folder file state + + String[] paths = FileName.splitPath(dir); + if ( paths[0] != null && paths[0].length() > 1) + { + // Get the file state for the parent folder + + FileState parentState = getStateForPath(tree, paths[0]); + if ( parentState == null && ctx.hasStateTable()) + parentState = ctx.getStateTable().findFileState( paths[0], true, true); + + // Update the modification timestamp + + parentState.updateModifyDateTime(); + } + } } else throw new DirectoryNotEmptyException( dir); @@ -1970,6 +2109,21 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa if ( fstate.decrementOpenCount() == 0) fstate.setSharedAccess( SharingMode.READWRITE + SharingMode.DELETE); + + // Check if there is a cached modification timestamp to be written out + + if ( file.hasDeleteOnClose() == false && fstate.hasModifyDateTime() && fstate.hasNodeRef()) { + + // Update the modification date on the file/folder node + + Date modifyDate = new Date( fstate.getModifyDateTime()); + nodeService.setProperty( fstate.getNodeRef(), ContentModel.PROP_MODIFIED, modifyDate); + + // Debug + + if ( logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILE)) + logger.debug("Updated modifcation timestamp, " + file.getFullName() + ", modTime=" + modifyDate); + } } } @@ -2087,7 +2241,27 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Remove the file state if ( ctx.hasStateTable()) + { + // Remove the file state + ctx.getStateTable().removeFileState(name); + + // Update, or create, a parent folder file state + + String[] paths = FileName.splitPath(name); + if ( paths[0] != null && paths[0].length() > 1) + { + // Get the file state for the parent folder + + FileState parentState = getStateForPath(tree, paths[0]); + if ( parentState == null && ctx.hasStateTable()) + parentState = ctx.getStateTable().findFileState( paths[0], true, true); + + // Update the modification timestamp + + parentState.updateModifyDateTime(); + } + } } // Debug @@ -2575,15 +2749,17 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa beginWriteTransaction( sess); - // Set the creation date on the file/folder node + // Set the modification date on the file/folder node Date modifyDate = new Date( info.getModifyDateTime()); nodeService.setProperty( nodeRef, ContentModel.PROP_MODIFIED, modifyDate); - // Update the change date/time + // Update the change date/time, clear the cached modification date/time - if ( fstate != null) + if ( fstate != null) { fstate.updateChangeDateTime(); + fstate.updateModifyDateTime( 0L); + } // DEBUG diff --git a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java index 8f87e281c0..97bfa7d2a1 100644 --- a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java @@ -224,6 +224,8 @@ public class ContentNetworkFile extends NodeRefNetworkFile StringBuilder str = new StringBuilder(); str.append( "["); + str.append(getFullName()); + str.append(","); str.append( getNodeRef().getId()); str.append( ",channel="); str.append( channel); diff --git a/source/java/org/alfresco/filesys/repo/ContentSearchContext.java b/source/java/org/alfresco/filesys/repo/ContentSearchContext.java index 02d27838c0..4d0ad9a268 100644 --- a/source/java/org/alfresco/filesys/repo/ContentSearchContext.java +++ b/source/java/org/alfresco/filesys/repo/ContentSearchContext.java @@ -496,4 +496,40 @@ public class ContentSearchContext extends SearchContext return false; } + + /** + * Check if the search is returning pseudo files or real file entries + * + * @return boolean + */ + protected boolean returningPseudoFiles() { + return donePseudoFiles ? true : false; + } + + /** + * Return the relative path that is being searched + * + * @return String + */ + protected String getRelativePath() { + return m_relPath; + } + + /** + * Return the results array size + * + * @return int + */ + protected int getResultsSize() { + return results != null ? results.size() : 0; + } + + /** + * Return the pseudo file list size + * + * @return int + */ + protected int getPseudoListSize() { + return pseudoList != null ? pseudoList.numberOfFiles() : 0; + } } diff --git a/source/java/org/alfresco/filesys/repo/NodeMonitor.java b/source/java/org/alfresco/filesys/repo/NodeMonitor.java index 11276214e7..05a6b6b0f7 100644 --- a/source/java/org/alfresco/filesys/repo/NodeMonitor.java +++ b/source/java/org/alfresco/filesys/repo/NodeMonitor.java @@ -551,25 +551,30 @@ public class NodeMonitor extends TransactionListenerAdapter return null; } + // check for a node delete + + if ( nodeEvent instanceof DeleteNodeEvent) { + + // Node deleted + + processDeleteNode((DeleteNodeEvent) nodeEvent); + } + // Check that the node is still valid - if (!m_nodeService.exists(nodeEvent.getNodeRef())) + else if (!m_nodeService.exists(nodeEvent.getNodeRef())) { return null; } + // Process the node event, for an existing node + 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 @@ -745,7 +750,7 @@ public class NodeMonitor extends TransactionListenerAdapter if ( m_changeHandler.getGlobalNotifyMask() != 0) { - // Send a file created event to the change notification handler + // Send a file deleted event to the change notification handler if ( deleteEvent.getFileType() == FileFolderServiceType.FILE) m_changeHandler.notifyFileChanged(NotifyChange.ActionRemoved, relPath); diff --git a/source/java/org/alfresco/filesys/state/FileStateListener.java b/source/java/org/alfresco/filesys/state/FileStateListener.java new file mode 100644 index 0000000000..ac3a934063 --- /dev/null +++ b/source/java/org/alfresco/filesys/state/FileStateListener.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2006-2009 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.state; + +/** + * File State Listener Interface + * + * @author gkspencer + */ +public interface FileStateListener { + + /** + * File state has expired. The listener can control whether the file state is removed + * from the cache, or not. + * + * @param state FileState + * @return true to remove the file state from the cache, or false to leave the file state in the cache + */ + public boolean fileStateExpired(FileState state); + + /** + * File state cache is closing down, any resources attached to the file state must be released. + * + * @param state FileState + */ + public void fileStateClosed(FileState state); +} diff --git a/source/java/org/alfresco/filesys/state/FileStateTable.java b/source/java/org/alfresco/filesys/state/FileStateTable.java index 9f9f5fb577..eb2f2ce6d8 100644 --- a/source/java/org/alfresco/filesys/state/FileStateTable.java +++ b/source/java/org/alfresco/filesys/state/FileStateTable.java @@ -51,6 +51,10 @@ public class FileStateTable private long m_cacheTimer = 2 * 60000L; // 2 minutes default + // File state listener, can veto expiring of file states + + private FileStateListener m_stateListener; + /** * Class constructor */ @@ -287,6 +291,11 @@ public class FileStateTable FileState state = m_stateTable.get(enm.nextElement()); + // Check if there is a state listener + + if ( m_stateListener != null) + m_stateListener.fileStateClosed(state); + // DEBUG if (logger.isDebugEnabled()) @@ -336,19 +345,23 @@ public class FileStateTable if (state.hasExpired(curTime) && state.getOpenCount() == 0) { - - // Remove the expired file state - - m_stateTable.remove(state.getPath()); - - // DEBUG - - if (logger.isDebugEnabled()) - logger.debug("Expired file state: " + state); - - // Update the expired count - - expiredCnt++; + // Check with the state listener before removing the file state, if enabled + + if ( hasStateListener() == false || m_stateListener.fileStateExpired( state) == true) + { + // Remove the expired file state + + m_stateTable.remove(state.getPath()); + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug("Expired file state: " + state); + + // Update the expired count + + expiredCnt++; + } } } } @@ -381,4 +394,32 @@ public class FileStateTable logger.debug(" " + fname + "(" + state.getSecondsToExpire(curTime) + ") : " + state); } } + + /** + * Add a file state listener + * + * @param l FileStateListener + */ + public final void addStateListener(FileStateListener l) { + m_stateListener = l; + } + + /** + * Remove a file state listener + * + * @param l FileStateListener + */ + public final void removeStateListener(FileStateListener l) { + if ( m_stateListener == l) + m_stateListener = null; + } + + /** + * Check if the file state listener is set + * + * @return boolean + */ + public final boolean hasStateListener() { + return m_stateListener != null ? true : false; + } } \ No newline at end of file