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}