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