package org.intellimate.izou.system.file; import org.intellimate.izou.util.IzouModule; import org.intellimate.izou.main.Main; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.nio.file.StandardWatchEventKinds.*; /** * The file manager listens for events that were caused by modifications made to property files and * then reloads the file. * * You can register an {@code AddOn} or a {@code ReloadableFile} with the path to the directory it is supposed to watch */ public class FileManager extends IzouModule implements Runnable { /** * Default java watching service for directories, raises events when changes happen in this directory */ private WatchService watcher; /** * Map that holds watchKeys (ID's) of the directories and the addOns using the directories */ private Map<WatchKey, List<FileInfo>> addOnMap; /** * Creates a new FileManager with a watcher and addOnMap * @param main an instance of main * @throws IOException exception is thrown by watcher service */ public FileManager(Main main) throws IOException { super(main); watcher = FileSystems.getDefault().newWatchService(); addOnMap = new HashMap<>(); getMain().getThreadPoolManager().getAddOnsThreadPool().submit(this); } /** * Use this method to register a file with the watcherService * * @param dir directory of file * @param fileType the name/extension of the file * IMPORTANT: Please try to always enter the full name with extension of the file (Ex: "test.txt"), * it would be best if the fileType is the full file name, and that the file name is clearly * distinguishable from other files. * For example, the property files are stored with the ID of the addon they belong too. That way * every property file is easily distinguishable. * @param reloadableFile object of interface that file belongs to * @throws IOException exception thrown by watcher service */ public void registerFileDir(Path dir, String fileType, ReloadableFile reloadableFile) throws IOException { WatchKey key = dir.register(watcher, ENTRY_MODIFY); List<FileInfo> fileInfos = addOnMap.get(key); if(fileInfos != null) { fileInfos.add(new FileInfo(dir, fileType, reloadableFile)); } else { fileInfos = new ArrayList<>(); fileInfos.add(new FileInfo(dir, fileType, reloadableFile)); addOnMap.put(key, fileInfos); } } /** * Checks if an event belongs to the desired file type * * @param event the event to check * @return the boolean value corresponding to the output */ private boolean isFileType(WatchEvent event, String fileType) { return event.context().toString().contains(fileType); } /** * Writes default file to real file * The default file would be a file that can be packaged along with the code, from which a real file (say a * properties file for example) can be loaded. This is useful because there are files (like property files0 that * cannot be shipped with the package and have to be created at runtime. To still be able to fill these files, you * can create a default file (usually txt) from which the content, as mentioned above, can then be loaded into the * real file. * * @param defaultFilePath path to default file (or where it should be created) * @param realFilePath path to real file (that should be filled with content of default file) * @return true if operation has succeeded, else false */ public boolean writeToFile(String defaultFilePath, String realFilePath) { try { Files.copy(Paths.get(defaultFilePath), Paths.get(realFilePath), StandardCopyOption.REPLACE_EXISTING); return true; } catch (IOException e) { error("Unable to write to copy Properties-File", e); return false; } } /** * Creates a default File in case it does not exist yet. Default files can be used to load other files that are * created at runtime (like properties file) * * @param defaultFilePath path to default file.txt (or where it should be created) * @param initMessage the string to write in default file * @throws IOException is thrown by bufferedWriter */ public void createDefaultFile(String defaultFilePath, String initMessage) throws IOException { File file = new File(defaultFilePath); BufferedWriter bufferedWriterInit = null; try { if (!file.exists()) { file.createNewFile(); bufferedWriterInit = new BufferedWriter(new FileWriter(defaultFilePath)); bufferedWriterInit.write(initMessage); } } catch (IOException e) { error("unable to create the Default-File", e); } finally { if(bufferedWriterInit != null) { try { bufferedWriterInit.close(); } catch (IOException e) { error("Unable to close input stream", e); } } } } /** * Checks if {@code fileInfo} and {@code key} match each other, in which case the fileInfo and key are processed * * @param key current key * @param fileInfos all fileInfos that match key */ private void checkAndProcessFileInfo(WatchKey key, List<FileInfo> fileInfos) { for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); for (FileInfo fileInfo : fileInfos) { if (kind == OVERFLOW) { try { throw new IncompleteFileEventException(); } catch (IncompleteFileEventException e) { log.warn(e); } } else if ((kind == ENTRY_CREATE || kind == ENTRY_MODIFY || kind == ENTRY_DELETE) && isFileType(event, fileInfo.getFileType())) { try { if (fileInfo.getReloadableFile() != null) { fileInfo.getReloadableFile().reloadFile(kind.toString()); debug("Reloaded file: " + event.context().toString()); getMain().getFilePublisher().notifyFileSubscribers(fileInfo.getReloadableFile()); } } catch (Exception e) { log.warn(e); } try { Thread.sleep(1000); } catch (InterruptedException e) { log.warn(e); } } } } } /** * Main method of fileManager, it constantly waits for new events and then processes them */ @Override public void run() { while(true) { WatchKey key; try { key = watcher.take(); } catch (InterruptedException e) { log.warn(e); continue; } List<FileInfo> fileInfos = addOnMap.get(key); checkAndProcessFileInfo(key, fileInfos); // reset key and remove from set if directory no longer accessible boolean valid = key.reset(); if (!valid) { addOnMap.remove(key); // all directories are inaccessible if (addOnMap.isEmpty()) { break; } } } } /** * Exception thrown if there are multiple Events fired at the same time. */ @SuppressWarnings("WeakerAccess") public class IncompleteFileEventException extends Exception { public IncompleteFileEventException() { super("Fired file event has been lost or discarded"); } } }