001package com.hfg.util; 002 003import java.io.File; 004import java.io.FileFilter; 005import java.nio.file.Path; 006import java.nio.file.StandardWatchEventKinds; 007import java.nio.file.WatchEvent; 008import java.util.Date; 009import java.util.List; 010import java.util.ArrayList; 011import java.util.Map; 012import java.util.HashMap; 013 014import com.hfg.util.collection.CollectionUtil; 015import com.hfg.xml.XMLNode; 016import com.hfg.xml.XMLTag; 017 018//------------------------------------------------------------------------------ 019/** 020 * Identifies files that change between two points in time. 021 * @author J. Alex Taylor, hairyfatguy.com 022 */ 023//------------------------------------------------------------------------------ 024// com.hfg XML/HTML Coding Library 025// 026// This library is free software; you can redistribute it and/or 027// modify it under the terms of the GNU Lesser General Public 028// License as published by the Free Software Foundation; either 029// version 2.1 of the License, or (at your option) any later version. 030// 031// This library is distributed in the hope that it will be useful, 032// but WITHOUT ANY WARRANTY; without even the implied warranty of 033// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 034// Lesser General Public License for more details. 035// 036// You should have received a copy of the GNU Lesser General Public 037// License along with this library; if not, write to the Free Software 038// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 039// 040// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 041// jataylor@hairyfatguy.com 042//------------------------------------------------------------------------------ 043 044 045public class FileTreeSnapshot 046{ 047 private String mRootPath; 048 private Long mSnapshotTimestamp; 049 private Map<String, Long> mFileTimestampMap; 050 private int mMaxDepth = -1; 051 private FileFilter mFilter; 052 053 //########################################################################## 054 // CONSTRUCTORS 055 //########################################################################## 056 057 //-------------------------------------------------------------------------- 058 public FileTreeSnapshot(File inRoot) 059 { 060 this(inRoot, null); 061 } 062 063 //-------------------------------------------------------------------------- 064 public FileTreeSnapshot(File inRoot, int inMaxDepth) 065 { 066 this(inRoot, null, inMaxDepth); 067 } 068 069 //-------------------------------------------------------------------------- 070 public FileTreeSnapshot(File inRoot, FileFilter inFilter) 071 { 072 this(inRoot, inFilter, -1); 073 } 074 075 //-------------------------------------------------------------------------- 076 public FileTreeSnapshot(File inRoot, FileFilter inFilter, int inMaxDepth) 077 { 078 mFilter = inFilter; 079 mMaxDepth = inMaxDepth; 080 mFileTimestampMap = new HashMap<>(); 081 mRootPath = inRoot.getPath() + (inRoot.isDirectory() ? "/" : ""); 082 mSnapshotTimestamp = System.currentTimeMillis(); 083 recursivelyExtract(inRoot, 0); 084 } 085 086 //-------------------------------------------------------------------------- 087 public FileTreeSnapshot(XMLNode inXMLNode) 088 { 089 this(inXMLNode, null); 090 } 091 092 //-------------------------------------------------------------------------- 093 public FileTreeSnapshot(XMLNode inXMLNode, FileFilter inFilter) 094 { 095 mFilter = inFilter; 096 097 if (! inXMLNode.getTagName().equalsIgnoreCase("FileTreeSnapshot")) 098 { 099 throw new RuntimeException("Cannont create a " + this.getClass().getSimpleName() + " from an " + inXMLNode.getTagName() + " tag!"); 100 } 101 102 mRootPath = inXMLNode.getAttributeValue("root"); 103 104 String timestampString = inXMLNode.getAttributeValue("timestamp"); 105 if (StringUtil.isSet(timestampString)) 106 { 107 mSnapshotTimestamp = Long.parseLong(timestampString); 108 } 109 110 mFileTimestampMap = new HashMap<>(); 111 112 for (XMLNode subtag : inXMLNode.getXMLNodeSubtags()) 113 { 114 mFileTimestampMap.put(subtag.getAttributeValue("path"), Long.parseLong(subtag.getAttributeValue("lastModified"))); 115 } 116 } 117 118 //########################################################################## 119 // PUBLIC METHODS 120 //########################################################################## 121 122 //-------------------------------------------------------------------------- 123 public XMLNode toXMLNode() 124 { 125 XMLTag rootTag = new XMLTag("FileTreeSnapshot"); 126 rootTag.setAttribute("root", mRootPath); 127 rootTag.setAttribute("timestamp", mSnapshotTimestamp); 128 129 for (String path : mFileTimestampMap.keySet()) 130 { 131 XMLTag fileTag = new XMLTag("File"); 132 fileTag.setAttribute("path", path); 133 fileTag.setAttribute("lastModified", mFileTimestampMap.get(path)); 134 rootTag.addSubtag(fileTag); 135 } 136 137 return rootTag; 138 } 139 140 //-------------------------------------------------------------------------- 141 public Date getSnapshotDate() 142 { 143 return (mSnapshotTimestamp != null ? new Date(mSnapshotTimestamp) : null); 144 } 145 146 //-------------------------------------------------------------------------- 147 public List<File> getFiles() 148 { 149 List<File> files = new ArrayList<>(mFileTimestampMap.size()); 150 for (String path : mFileTimestampMap.keySet()) 151 { 152 files.add(new File(path)); 153 } 154 155 return files; 156 } 157 158 //-------------------------------------------------------------------------- 159 /** 160 Returns a list of files that are present in the specified file root but not 161 in the snapshot or files that are present in both the specified file root 162 and the snapshot but whose last modification dates are different. 163 * @param inRoot 164 */ 165 public List<File> getModifiedFiles(File inRoot) 166 { 167 List<File> changedFiles = new ArrayList<>(); 168 169 recursivelyCompare(inRoot, changedFiles, inRoot.getPath() + (inRoot.isDirectory() ? "/" : ""), 0); 170 171 return changedFiles; 172 } 173 174 //-------------------------------------------------------------------------- 175 /** 176 Returns a list of create/modify/delete events for files changed relative to the snapshot. 177 * @param inRoot file root to compare with the snapshot 178 */ 179 public List<WatchEvent<Path>> getEvents(File inRoot) 180 { 181 List<WatchEvent<Path>> events = new ArrayList<>(); 182 183 Map<String, Long> fileMapCopy = new HashMap<>(mFileTimestampMap); 184 185 recursivelyExtractEvents(inRoot, events, inRoot.getPath() + (inRoot.isDirectory() ? "/" : ""), fileMapCopy, 0); 186 187 // Any entries remaining in the copy of the file map must have been deleted since the snapshot 188 if (CollectionUtil.hasValues(fileMapCopy)) 189 { 190 for (String filePath : fileMapCopy.keySet()) 191 { 192 events.add(new FileEvent(StandardWatchEventKinds.ENTRY_DELETE, new File(inRoot, filePath).toPath())); 193 } 194 } 195 196 return events; 197 } 198 199 //########################################################################## 200 // PRIVATE METHODS 201 //########################################################################## 202 203 //-------------------------------------------------------------------------- 204 private void recursivelyExtract(File inFile, int inCurrentDepth) 205 { 206 if (inFile.isDirectory()) 207 { 208 if (mMaxDepth <= 0 || inCurrentDepth < mMaxDepth) 209 { 210 for (File file : inFile.listFiles()) 211 { 212 recursivelyExtract(file, inCurrentDepth + 1); 213 } 214 } 215 } 216 else if (null == mFilter 217 || mFilter.accept(inFile)) 218 { 219 String rootStrippedPath = inFile.getPath().substring(inFile.getPath().indexOf(mRootPath) + mRootPath.length()); 220 mFileTimestampMap.put(rootStrippedPath, inFile.lastModified()); 221 } 222 } 223 224 //-------------------------------------------------------------------------- 225 private void recursivelyCompare(File inFile, List<File> inChangedFiles, String inRootPath, int inCurrentDepth) 226 { 227 if (inFile.isDirectory()) 228 { 229 if (mMaxDepth <= 0 || inCurrentDepth < mMaxDepth) 230 { 231 for (File file : inFile.listFiles()) 232 { 233 recursivelyCompare(file, inChangedFiles, inRootPath, inCurrentDepth + 1); 234 } 235 } 236 } 237 else if (null == mFilter 238 || mFilter.accept(inFile)) 239 { 240 String rootStrippedPath = inFile.getPath().substring(inFile.getPath().indexOf(inRootPath) + inRootPath.length()); 241 242 Long snapshotLastModifiedTime = mFileTimestampMap.get(rootStrippedPath); 243 if (null == snapshotLastModifiedTime 244 || snapshotLastModifiedTime != inFile.lastModified()) 245 { 246 inChangedFiles.add(inFile); 247 } 248 } 249 } 250 251 //-------------------------------------------------------------------------- 252 private void recursivelyExtractEvents(File inFile, List<WatchEvent<Path>> inEvents, String inRootPath, Map<String, Long> inPrevFileMap, int inCurrentDepth) 253 { 254 String rootStrippedPath = inFile.getPath().substring(inFile.getPath().indexOf(inRootPath) + inRootPath.length()); 255 Long snapshotLastModifiedTime = inPrevFileMap.remove(rootStrippedPath); 256 257 if (inFile.isDirectory()) 258 { 259 if (mMaxDepth <= 0 || inCurrentDepth < mMaxDepth) 260 { 261 for (File file : inFile.listFiles()) 262 { 263 recursivelyExtractEvents(file, inEvents, inRootPath, inPrevFileMap, inCurrentDepth + 1); 264 } 265 } 266 } 267 else if (null == mFilter 268 || mFilter.accept(inFile)) 269 { 270 if (null == snapshotLastModifiedTime) 271 { 272 inEvents.add(new FileEvent(StandardWatchEventKinds.ENTRY_CREATE, inFile.toPath())); 273 } 274 else if (snapshotLastModifiedTime != inFile.lastModified()) 275 { 276 inEvents.add(new FileEvent(StandardWatchEventKinds.ENTRY_MODIFY, inFile.toPath())); 277 } 278 } 279 } 280 281 //########################################################################## 282 // PRIVATE CLASS 283 //########################################################################## 284 285 private class FileEvent implements WatchEvent<Path> 286 { 287 private Path mPath; 288 private WatchEvent.Kind<Path> mKind; 289 290 //----------------------------------------------------------------------- 291 public FileEvent(WatchEvent.Kind<Path> inEventType, Path inPath) 292 { 293 mKind = inEventType; 294 mPath = inPath; 295 } 296 297 //----------------------------------------------------------------------- 298 @Override 299 public String toString() 300 { 301 return mKind.name() + ": " + mPath; 302 } 303 304 //----------------------------------------------------------------------- 305 public WatchEvent.Kind<Path> kind() 306 { 307 return mKind; 308 } 309 310 //----------------------------------------------------------------------- 311 public int count() 312 { 313 return 1; 314 } 315 316 //----------------------------------------------------------------------- 317 public Path context() 318 { 319 return mPath; 320 } 321 } 322}