/*******************************************************************************
* Copyright (c) 2016 ARM Ltd. and others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* ARM Ltd and ARM Germany GmbH - Initial API and implementation
*******************************************************************************/
package com.arm.cmsis.pack.utils;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileTime;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Monitors file system for creation, change and deletion of given file(s)
*/
public abstract class FileChangeWatcher {
// flags for watch events (to isolate extender classes from StandardWatchEventKinds)
public static final int CREATE = 1;
public static final int MODIFY = 2;
public static final int DELETE = 4;
public static final int ALL = CREATE|MODIFY|DELETE;
protected WatchService watcher;
protected Map<WatchKey, Path> dirKeys; // we actually monitor directories
protected Map<String, Long> filesToWatch; // with their modification times
protected int watchFlags;
protected WatchingThread thread;
protected WatchEvent.Kind<?>[] watchEventKinds;
protected boolean containsWildcards = false; // flag indicating that filesToWatch contains entries with wildcards
class WatchingThread extends Thread { // FileChageWatcher cannot extend Thread because it must be able to restart
@Override
public void run() {
processWatchEvents();
}
}
/**
* Default constructor, registers for all events
* @throws IOException
*/
protected FileChangeWatcher() throws IOException {
this(ALL);
}
/**
* Main constructor, registers for specified events
* @param flags a combination of CREATE, MODIFY and DELETE flags. if 0 the behavior is undefined
* @throws IOException
*/
protected FileChangeWatcher(int flags) {
dirKeys = new HashMap<>();
filesToWatch = new HashMap<>();
watchFlags = flags;
watchEventKinds = getEventKinds();
thread = null;
try {
watcher = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
e.printStackTrace();
watcher = null;
}
}
/**
* Single file constructor: cannot be used if an extender redefines registerFile() or startWatch()
* @param file file to watch
* @param flags a combination of CREATE, MODIFY and DELETE flags. if 0 the behavior is undefined
* @throws IOException
*/
protected FileChangeWatcher(String file, int flags) {
this(flags);
registerFile(file);
startWatch();
}
/**
* Stops watch and clears all keys
*/
public void clearWatch() {
stopWatch();
filesToWatch.clear();
containsWildcards = false;
for(WatchKey key : dirKeys.keySet()) {
key.cancel();
}
dirKeys.clear();
}
public synchronized void startWatch() {
if(watcher == null) {
return;
}
if(dirKeys.isEmpty()) {
return;
}
if(thread != null && thread.isAlive()) {
return;
}
thread = new WatchingThread();
thread.start();
}
public synchronized void stopWatch() {
if(thread != null && thread.isAlive()) {
thread.interrupt();
}
thread = null;
}
/**
* Registers file for watching, the file does not need to exists if watcher monitors creation,
* @param file absolute filename to register for watching, wildcards are allowed in the name and extension
* @throws IOException
*/
public synchronized void registerFile(String file) {
if(file == null || file.isEmpty()) {
return;
}
if(filesToWatch.containsKey(file)) {
return;
}
if(file.indexOf('*') > 0 || file.indexOf('&') > 0 )
containsWildcards = true;
File f = new File(file);
long modified = 0;
if(f.exists()) {
modified = f.lastModified();
}
filesToWatch.put(file, modified);
String dir = Utils.extractPath(file, false);
Path path = Paths.get(dir);
registerDir(path);
}
/**
* Removes file from watching.
* @param file absolute filename to stop watching, must be the same as passed to regiterFile()
* @throws IOException
*/
public synchronized void removeFile(String file) {
if(filesToWatch.containsKey(file)) {
return;
}
filesToWatch.remove(file);
containsWildcards = false;
for(String f : filesToWatch.keySet()) {
if(f.indexOf('*') > 0 || f.indexOf('&') > 0 ) {
containsWildcards = true;
break;
}
}
String dir = Utils.extractPath(file, false);
if(isDirToWatch(dir)) {
return;
}
WatchKey key = getKey(dir);
if(key != null) {
dirKeys.remove(key);
key.cancel();
}
if(dirKeys.isEmpty())
clearWatch();
}
protected WatchKey getKey(String dir) {
Path path = Paths.get(dir);
if(path == null) {
return null;
}
WatchKey key = null;
for(Entry<WatchKey, Path> e : dirKeys.entrySet()) {
Path p = e.getValue();
if(path.equals(p)) {
return e.getKey();
}
}
return key;
}
protected synchronized void registerDir(Path path){
if(dirKeys.containsValue(path)) {
return;
}
if(watcher == null) {
return;
}
WatchKey key;
try {
key = path.register(watcher, watchEventKinds);
} catch (IOException e) {
e.printStackTrace();
return;
}
dirKeys.put(key, path);
}
protected WatchEvent.Kind<?>[] getEventKinds() {
List<WatchEvent.Kind<Path> > eventList = new LinkedList<>();
if((watchFlags & CREATE) == CREATE) {
eventList.add(StandardWatchEventKinds.ENTRY_CREATE);
}
if((watchFlags & MODIFY) == MODIFY) {
eventList.add(StandardWatchEventKinds.ENTRY_MODIFY);
}
if((watchFlags & DELETE) == DELETE) {
eventList.add(StandardWatchEventKinds.ENTRY_DELETE);
}
return eventList.toArray(new WatchEvent.Kind<?>[0]);
}
static public int eventKindToInt(Kind<?> kind) {
if(kind == StandardWatchEventKinds.ENTRY_CREATE) {
return CREATE;
} else if(kind == StandardWatchEventKinds.ENTRY_MODIFY) {
return MODIFY;
} else if(kind == StandardWatchEventKinds.ENTRY_DELETE) {
return DELETE;
}
return 0;
}
/**
* Checks if directory is registered for watching
* @param dir absolute directory pathname
* @return true if to watch
*/
public boolean isDirToWatch(String dir) {
for(String file : filesToWatch.keySet()) {
if(file.startsWith(dir)) {
return true;
}
}
return false;
}
/**
* Checks if a file is registered for watching
* @param file absolute filename, wildcards are allowed in the name segment
* @return 0 if file not watched, 1 if watched explicitly, -1 if implicitly via wildcards
*/
public int isFileToWatch(String file) {
if(filesToWatch.containsKey(file)) {
return 1;
}
for(String f: filesToWatch.keySet()) {
if(WildCards.match(file, f)) {
return -1;
}
}
return 0;
}
protected void processWatchEvents() {
while(true) {
// wait for key to be signaled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
return;
}
Path dir = dirKeys.get(key);
if (dir == null) {
continue; // not our directory
}
for (WatchEvent<?> event: key.pollEvents()) {
Kind<?> eventKind = event.kind();
int kind = eventKindToInt(eventKind);
if((kind & watchFlags) == 0) {
continue; // not interested, ignore it
}
// Context for directory entry event is the file name of entry
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path name = ev.context();
String file = dir.toString().replace('\\', '/') + '/'+ name;
int watched = isFileToWatch(file);
if(watched == 0 ) {
continue;
}
long diff = 0;
if(watched == 1) {
long lastModified = 0;
File f = new File(file);
if(kind != DELETE) {
lastModified = f.lastModified();
}
diff = lastModified - filesToWatch.get(file);
filesToWatch.put(file, lastModified); // anyway update the entry
}
boolean changed = kind != MODIFY || diff != 0 || watched < 0;
if(changed && key.isValid()) {
action(file, kind);
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
dirKeys.remove(key);
// all directories are inaccessible
if (dirKeys.isEmpty()) {
break;
}
}
}
}
/**
* Executes an action on file change
* @param file absolute filename
* @param kind one of CREATE, MODIFIED or DELETE values
*/
abstract protected void action(String file, int kind);
/**
* Sets file' last modified time to the current system tyme
* @param file absolute filename
*/
public static void touchFile(String file) {
if(file == null || file.isEmpty()) {
return;
}
Path path = Paths.get(file);
try {
Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
} catch (IOException e) {
// do nothing
}
}
public static void createDirectories(String dir) throws IOException {
if(dir == null || dir.isEmpty()) {
return;
}
Path path = Paths.get(dir);
Files.createDirectories(path);
}
}