001package org.intellimate.izou.system.file; 002 003import org.intellimate.izou.util.IzouModule; 004import org.intellimate.izou.main.Main; 005 006import java.io.BufferedWriter; 007import java.io.File; 008import java.io.FileWriter; 009import java.io.IOException; 010import java.nio.file.*; 011import java.util.ArrayList; 012import java.util.HashMap; 013import java.util.List; 014import java.util.Map; 015 016import static java.nio.file.StandardWatchEventKinds.*; 017 018/** 019 * The file manager listens for events that were caused by modifications made to property files and 020 * then reloads the file. 021 * 022 * You can register an {@code AddOn} or a {@code ReloadableFile} with the path to the directory it is supposed to watch 023 */ 024public class FileManager extends IzouModule implements Runnable { 025 /** 026 * Default java watching service for directories, raises events when changes happen in this directory 027 */ 028 private WatchService watcher; 029 030 /** 031 * Map that holds watchKeys (ID's) of the directories and the addOns using the directories 032 */ 033 private Map<WatchKey, List<FileInfo>> addOnMap; 034 035 /** 036 * Creates a new FileManager with a watcher and addOnMap 037 * @param main an instance of main 038 * @throws IOException exception is thrown by watcher service 039 */ 040 public FileManager(Main main) throws IOException { 041 super(main); 042 watcher = FileSystems.getDefault().newWatchService(); 043 addOnMap = new HashMap<>(); 044 getMain().getThreadPoolManager().getAddOnsThreadPool().submit(this); 045 } 046 047 /** 048 * Use this method to register a file with the watcherService 049 * 050 * @param dir directory of file 051 * @param fileType the name/extension of the file 052 * IMPORTANT: Please try to always enter the full name with extension of the file (Ex: "test.txt"), 053 * it would be best if the fileType is the full file name, and that the file name is clearly 054 * distinguishable from other files. 055 * For example, the property files are stored with the ID of the addon they belong too. That way 056 * every property file is easily distinguishable. 057 * @param reloadableFile object of interface that file belongs to 058 * @throws IOException exception thrown by watcher service 059 */ 060 public void registerFileDir(Path dir, String fileType, ReloadableFile reloadableFile) throws IOException { 061 WatchKey key = dir.register(watcher, ENTRY_MODIFY); 062 List<FileInfo> fileInfos = addOnMap.get(key); 063 064 if(fileInfos != null) { 065 fileInfos.add(new FileInfo(dir, fileType, reloadableFile)); 066 } else { 067 fileInfos = new ArrayList<>(); 068 fileInfos.add(new FileInfo(dir, fileType, reloadableFile)); 069 addOnMap.put(key, fileInfos); 070 } 071 } 072 073 /** 074 * Checks if an event belongs to the desired file type 075 * 076 * @param event the event to check 077 * @return the boolean value corresponding to the output 078 */ 079 private boolean isFileType(WatchEvent event, String fileType) { 080 return event.context().toString().contains(fileType); 081 } 082 083 /** 084 * Writes default file to real file 085 * The default file would be a file that can be packaged along with the code, from which a real file (say a 086 * properties file for example) can be loaded. This is useful because there are files (like property files0 that 087 * cannot be shipped with the package and have to be created at runtime. To still be able to fill these files, you 088 * can create a default file (usually txt) from which the content, as mentioned above, can then be loaded into the 089 * real file. 090 * 091 * @param defaultFilePath path to default file (or where it should be created) 092 * @param realFilePath path to real file (that should be filled with content of default file) 093 * @return true if operation has succeeded, else false 094 */ 095 public boolean writeToFile(String defaultFilePath, String realFilePath) { 096 try { 097 Files.copy(Paths.get(defaultFilePath), Paths.get(realFilePath), StandardCopyOption.REPLACE_EXISTING); 098 return true; 099 } catch (IOException e) { 100 error("Unable to write to copy Properties-File", e); 101 return false; 102 } 103 } 104 105 /** 106 * Creates a default File in case it does not exist yet. Default files can be used to load other files that are 107 * created at runtime (like properties file) 108 * 109 * @param defaultFilePath path to default file.txt (or where it should be created) 110 * @param initMessage the string to write in default file 111 * @throws IOException is thrown by bufferedWriter 112 */ 113 public void createDefaultFile(String defaultFilePath, String initMessage) throws IOException { 114 File file = new File(defaultFilePath); 115 BufferedWriter bufferedWriterInit = null; 116 try { 117 if (!file.exists()) { 118 file.createNewFile(); 119 bufferedWriterInit = new BufferedWriter(new FileWriter(defaultFilePath)); 120 bufferedWriterInit.write(initMessage); 121 } 122 } catch (IOException e) { 123 error("unable to create the Default-File", e); 124 } finally { 125 if(bufferedWriterInit != null) { 126 try { 127 bufferedWriterInit.close(); 128 } catch (IOException e) { 129 error("Unable to close input stream", e); 130 } 131 } 132 } 133 } 134 135 /** 136 * Checks if {@code fileInfo} and {@code key} match each other, in which case the fileInfo and key are processed 137 * 138 * @param key current key 139 * @param fileInfos all fileInfos that match key 140 */ 141 private void checkAndProcessFileInfo(WatchKey key, List<FileInfo> fileInfos) { 142 for (WatchEvent<?> event : key.pollEvents()) { 143 WatchEvent.Kind kind = event.kind(); 144 145 for (FileInfo fileInfo : fileInfos) { 146 if (kind == OVERFLOW) { 147 try { 148 throw new IncompleteFileEventException(); 149 } catch (IncompleteFileEventException e) { 150 log.warn(e); 151 } 152 } else if ((kind == ENTRY_CREATE || kind == ENTRY_MODIFY || kind == ENTRY_DELETE) 153 && isFileType(event, fileInfo.getFileType())) { 154 try { 155 if (fileInfo.getReloadableFile() != null) { 156 fileInfo.getReloadableFile().reloadFile(kind.toString()); 157 debug("Reloaded file: " + event.context().toString()); 158 getMain().getFilePublisher().notifyFileSubscribers(fileInfo.getReloadableFile()); 159 } 160 } catch (Exception e) { 161 log.warn(e); 162 } 163 try { 164 Thread.sleep(1000); 165 } catch (InterruptedException e) { 166 log.warn(e); 167 } 168 } 169 } 170 } 171 } 172 173 /** 174 * Main method of fileManager, it constantly waits for new events and then processes them 175 */ 176 @Override 177 public void run() { 178 while(true) { 179 WatchKey key; 180 try { 181 key = watcher.take(); 182 } catch (InterruptedException e) { 183 log.warn(e); 184 continue; 185 } 186 187 List<FileInfo> fileInfos = addOnMap.get(key); 188 checkAndProcessFileInfo(key, fileInfos); 189 190 // reset key and remove from set if directory no longer accessible 191 boolean valid = key.reset(); 192 if (!valid) { 193 addOnMap.remove(key); 194 195 // all directories are inaccessible 196 if (addOnMap.isEmpty()) { 197 break; 198 } 199 } 200 } 201 } 202 203 /** 204 * Exception thrown if there are multiple Events fired at the same time. 205 */ 206 @SuppressWarnings("WeakerAccess") 207 public class IncompleteFileEventException extends Exception { 208 public IncompleteFileEventException() { 209 super("Fired file event has been lost or discarded"); 210 } 211 } 212} 213