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;
}
}