package org.jabref.collab; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import org.jabref.logic.util.io.FileUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This thread monitors a set of files, each associated with a FileUpdateListener, for changes * in the file's last modification time stamp. The */ public class FileUpdateMonitor implements Runnable { private static final Log LOGGER = LogFactory.getLog(FileUpdateMonitor.class); private static final int WAIT = 4000; private int numberOfUpdateListener; private final Map<String, Entry> entries = new HashMap<>(); @Override public void run() { // The running variable is used to make the thread stop when needed. while (true) { for (Entry e : entries.values()) { try { if (e.hasBeenUpdated()) { e.notifyListener(); } } catch (IOException ex) { e.notifyFileRemoved(); } } // Sleep for a while before starting a new polling round. try { Thread.sleep(WAIT); } catch (InterruptedException ex) { LOGGER.debug("FileUpdateMonitor has been interrupted. Terminating...", ex); return; } } } /** * Add a new file to monitor. Returns a handle for accessing the entry. * @param ul FileUpdateListener The listener to notify when the file changes. * @param file File The file to monitor. * @throws IOException if the file does not exist. */ public String addUpdateListener(FileUpdateListener ul, File file) throws IOException { if (!file.exists()) { throw new IOException("File not found"); } numberOfUpdateListener++; String key = String.valueOf(numberOfUpdateListener); entries.put(key, new Entry(ul, file.toPath())); return key; } /** * Forces a check on the file, and returns the result. Does not * force a report to all listeners before the next routine check. */ public boolean hasBeenModified(String handle) { Entry entry = entries.get(handle); if (entry == null) { return false; } try { return entry.hasBeenUpdated(); } catch (IOException ex) { // Thrown if file has been removed. We return false. return false; } } /** * Change the stored timestamp for the given file. If the timestamp equals * the file's timestamp on disk, after this call the file will appear to * have been modified. Used if a file has been modified, and the change * scan fails, in order to ensure successive checks. * @param handle the handle to the correct file. */ public void perturbTimestamp(String handle) { Entry entry = entries.get(handle); if (entry != null) { entry.decreaseTimeStamp(); } } /** * Removes a listener from the monitor. * @param handle String The handle for the listener to remove. */ public void removeUpdateListener(String handle) { entries.remove(handle); } public void updateTimeStamp(String key) { Entry entry = entries.get(key); if (entry != null) { try { entry.updateTimeStamp(); } catch (IOException e) { LOGGER.error("Couldn't update timestamp", e); } } } /** * Method for getting the temporary file used for this database. The tempfile * is used for comparison with the changed on-disk version. * @param key String The handle for this monitor. * @throws IllegalArgumentException If the handle doesn't correspond to an entry. * @return File The temporary file. */ public Path getTempFile(String key) throws IllegalArgumentException { Entry entry = entries.get(key); if (entry == null) { throw new IllegalArgumentException("Entry not found"); } return entry.getTmpFile(); } /** * A class containing the File, the FileUpdateListener and the current time stamp for one file. */ static class Entry { private final FileUpdateListener listener; private final Path file; private final Path tmpFile; private long timeStamp; private long fileSize; public Entry(FileUpdateListener ul, Path f) throws IOException { listener = ul; file = f; timeStamp = Files.getLastModifiedTime(file).toMillis(); fileSize = Files.size(file); tmpFile = FileUpdateMonitor.getTempFile(); if (tmpFile != null) { tmpFile.toFile().deleteOnExit(); copy(); } } /** * Check if time stamp or the file size has changed. * @throws IOException if the file does no longer exist. * @return boolean true if the file has changed. */ public boolean hasBeenUpdated() throws IOException { long modified = Files.getLastModifiedTime(file).toMillis(); if (modified == 0L) { throw new IOException("File deleted"); } long fileSizeNow = Files.size(file); return (timeStamp != modified) || (fileSize != fileSizeNow); } public void updateTimeStamp() throws IOException { timeStamp = Files.getLastModifiedTime(file).toMillis(); if (timeStamp == 0L) { notifyFileRemoved(); } fileSize = Files.size(file); copy(); } public boolean copy() { return FileUtil.copyFile(file, tmpFile, true); } /** * Call the listener method to signal that the file has changed. */ public void notifyListener() throws IOException { // Update time stamp. timeStamp = Files.getLastModifiedTime(file).toMillis(); fileSize = Files.size(file); listener.fileUpdated(); } /** * Call the listener method to signal that the file has been removed. */ public void notifyFileRemoved() { listener.fileRemoved(); } public Path getTmpFile() { return tmpFile; } public void decreaseTimeStamp() { timeStamp--; } } private static synchronized Path getTempFile() { Path temporaryFile = null; try { temporaryFile = Files.createTempFile("jabref", null); temporaryFile.toFile().deleteOnExit(); } catch (IOException ex) { LOGGER.warn("Could not create temporary file.", ex); } return temporaryFile; } }