001package org.intellimate.izou.sdk.properties;
002
003import org.intellimate.izou.sdk.Context;
004import org.intellimate.izou.sdk.contentgenerator.EventListener;
005import org.intellimate.izou.sdk.events.CommonEvents;
006import org.intellimate.izou.sdk.util.AddOnModule;
007import org.intellimate.izou.system.file.ReloadableFile;
008
009import java.io.*;
010import java.nio.channels.FileChannel;
011import java.nio.channels.FileLock;
012import java.nio.channels.OverlappingFileLockException;
013import java.util.Properties;
014import java.util.function.Consumer;
015
016/**
017 * EventPropertiesManager manages all events written in the local_events.properties file. You can register (add)
018 * events to the file, and get events from the file. The file pretty much serves as a hub for event IDs.
019 */
020public class EventPropertiesAssistant extends AddOnModule implements ReloadableFile {
021
022    /**
023     * The path to the local_events.properties file
024     */
025    private final String eventPropertiesPath = getContext().getFiles().getPropertiesLocation() + File.separator +
026            "local_events.properties";
027    private Properties properties;
028
029    /**
030     * Creates a new EventPropertiesAssistant
031     * @param context the context to use
032     * @param id the id of the addon
033     */
034    public EventPropertiesAssistant(Context context, String id) {
035        super(context, id);
036        properties = new Properties();
037        try {
038            createIzouPropertiesFiles();
039            reloadFile(null);
040        } catch (IOException e) {
041            context.getLogger().error("Unable to initialize local_events.properties file", e);
042        }
043
044        try {
045            getContext().getFiles().registerFileDir(getContext().getFiles().getPropertiesLocation().toPath(), getID(), this);
046        } catch (IOException e) {
047            error("Unable to register EventPropertiesAssistant with file manager " +
048                    "(file reload service)", e);
049        }
050
051        registerStandardEvents();
052    }
053
054    /**
055     * registers the standard-events
056     */
057    private void registerStandardEvents() {
058        CommonEvents.Descriptors.stopListener(this).ifPresent(this::registerEventListener);
059        CommonEvents.Presence.generalLeavingListener(this).ifPresent(this::registerEventListener);
060        CommonEvents.Presence.generalListener(this).ifPresent(this::registerEventListener);
061        CommonEvents.Presence.leavingListener(this).ifPresent(this::registerEventListener);
062        CommonEvents.Presence.presenceListener(this).ifPresent(this::registerEventListener);
063        CommonEvents.Presence.strictLeavingListener(this).ifPresent(this::registerEventListener);
064        CommonEvents.Presence.strictListener(this).ifPresent(this::registerEventListener);
065        CommonEvents.Response.fullResponseListener(this).ifPresent(this::registerEventListener);
066        CommonEvents.Response.majorResponseListener(this).ifPresent(this::registerEventListener);
067        CommonEvents.Response.minorResponseListener(this).ifPresent(this::registerEventListener);
068        CommonEvents.Type.notificationListener(this).ifPresent(this::registerEventListener);
069        CommonEvents.Type.responseListener(this).ifPresent(this::registerEventListener);
070    }
071
072    private void createIzouPropertiesFiles() throws IOException {
073        String propertiesPath = getContext().getFiles().getPropertiesLocation() + File.separator +
074                "local_events.properties";
075
076        File file = new File(propertiesPath);
077        BufferedWriter bufferedWriterInit = null;
078        try {
079            if (!file.exists()) {
080                file.createNewFile();
081                bufferedWriterInit = new BufferedWriter(new FileWriter(propertiesPath));
082                bufferedWriterInit.write("# You can use this file to store an event ID with a key, or shortcut, " +
083                        " so that others can easily access and\n# fire it using the key");
084            }
085        } catch (IOException e) {
086            //error("unable to create the local_events file", e);
087        } finally {
088            if(bufferedWriterInit != null)
089                bufferedWriterInit.close();
090        }
091    }
092
093    /**
094     * gets the String containing the Properties-Path
095     * @return a String
096     */
097    public String getEventPropertiesPath() {
098        return eventPropertiesPath;
099    }
100
101    /**
102     * Gets the full event ID associated with the key {@code key}
103     *
104     * @param key the key of the full event ID
105     * @return the complete the event ID, or null if none is found
106     */
107    public String getEventID(String key) {
108        return (String) properties.get(key);
109    }
110
111    /**
112     * Registers or adds an event to the local_events.properties file with the informations found in the EventListener
113     *
114     * @param eventListener the eventListener to add
115     */
116    public void registerEventListener(EventListener eventListener) {
117        registerEventID(eventListener.getDescription(),
118                eventListener.getDescriptorID(), eventListener.getDescriptor());
119    }
120
121    /**
122     * Registers or adds an event to the local_events.properties file
123     *
124     * @param description a simple description of the Event
125     * @param key the key with which to store the event ID
126     * @param value the complete event ID
127     */
128    public void registerEventID(String description, String key, String value) {
129        BufferedWriter bufferedWriter;
130        FileOutputStream out = null;
131        try {
132            out = new FileOutputStream(eventPropertiesPath, true);
133            bufferedWriter = new BufferedWriter(new OutputStreamWriter(out));
134            doWithLock(out.getChannel(), lock -> {
135                unlockedReloadFile();
136                if (getEventID(key) != null) {
137                    return;
138                }
139                try {
140                    bufferedWriter.write("\n\n" + key + "_DESCRIPTION = " + description + "\n" + key + " = " + value);
141                    bufferedWriter.flush();
142                } catch (IOException e) {
143                    e.printStackTrace();
144                }
145            });
146        } catch (FileNotFoundException e) {
147            error("Unable find file", e);
148        } finally {
149            try {
150                if (out != null) {
151                    out.close();
152                }
153            } catch (IOException e) {
154                error("Unable to close lock", e);
155            }
156        }
157    }
158
159    /**
160     * executes with a lock
161     * @param channel the channel where the lock is acquired from
162     * @param consumer the consumer to execute
163     */
164    private void doWithLock(FileChannel channel, Consumer<FileLock> consumer) {
165        FileLock lock = null;
166        try {
167            while (lock == null) {
168                try {
169                    lock = channel.tryLock();
170                } catch (OverlappingFileLockException e) {
171                    Thread.sleep(500);
172                }
173            }
174            consumer.accept(lock);
175        } catch (IOException | InterruptedException e) {
176            error("Unable to write", e);
177        } finally {
178            try {
179                if (lock != null) {
180                    lock.release();
181                }
182            } catch (IOException e) {
183                error("Unable to close lock", e);
184            }
185        }
186    }
187
188    /**
189     * Unregisters or deletes an event from the local_events.properties file
190     *
191     * @param eventKey the key under which the complete event ID is stored in the properties file
192     */
193    public void unregisterEventID(String eventKey) {
194        properties.remove(eventKey + "_DESCRIPTION");
195        properties.remove(eventKey);
196
197        FileOutputStream out = null;
198        BufferedReader reader = null;
199        BufferedWriter writer = null;
200
201        try {
202            out = new FileOutputStream(eventPropertiesPath, true);
203
204            final File tempFile = new File(eventPropertiesPath + "temp.properties");
205            final BufferedReader readerFinal = new BufferedReader(new FileReader(eventPropertiesPath));
206            final BufferedWriter writerFinal = new BufferedWriter(new FileWriter(tempFile));
207
208            doWithLock(out.getChannel(), lock -> {
209                unlockedReloadFile();
210                if (getEventID(eventKey) != null) {
211                    return;
212                }
213
214                try {
215                    String currentLine = readerFinal.readLine();
216                    while(currentLine != null) {
217                        String trimmedLine = currentLine.trim();
218                        if(trimmedLine.equals(eventKey + "_DESCRIPTION") || trimmedLine.equals(eventKey)) continue;
219                        writerFinal.write(currentLine + System.getProperty("line.separator"));
220                        currentLine = readerFinal.readLine();
221                    }
222                } catch (IOException e) {
223                    e.printStackTrace();
224                }
225            });
226
227            reader = readerFinal;
228            writer = writerFinal;
229            tempFile.renameTo(new File(eventPropertiesPath));
230        } catch (IOException e) {
231            error("Unable find file", e);
232        } finally {
233            try {
234                if (out != null) {
235                    out.close();
236                }
237                if (writer != null) {
238                    writer.close();
239                }
240                if (reader != null) {
241                    reader.close();
242                }
243            } catch (IOException e) {
244                error("Unable to close lock", e);
245            }
246        }
247    }
248
249    private void unlockedReloadFile() {
250        Properties tmpProperties = new Properties();
251        BufferedReader in = null;
252        try {
253            File eventFile = new File(eventPropertiesPath);
254            FileInputStream fileInputStream = new FileInputStream(eventFile);
255            in = new BufferedReader(new InputStreamReader(fileInputStream, "UTF8"));
256            tmpProperties.load(in);
257            this.properties = tmpProperties;
258        } catch(IOException e) {
259            error("Error while trying to load local_events.properties", e);
260        } finally {
261            if (in != null) {
262                try {
263                    in.close();
264                } catch (IOException e) {
265                    error("Unable to close input stream", e);
266                }
267            }
268        }
269    }
270
271    @Override
272    public void reloadFile(String eventType) {
273        try (FileOutputStream outputStream = new FileOutputStream(eventPropertiesPath, true)) {
274            doWithLock(outputStream.getChannel(), lock ->  unlockedReloadFile());
275        } catch (IOException e) {
276            error("Unable to reload local_events.properties file", e);
277        }
278    }
279}