001package org.intellimate.izou.sdk.properties;
002
003import org.intellimate.izou.sdk.Context;
004import org.intellimate.izou.sdk.util.AddOnModule;
005import org.intellimate.izou.system.file.ReloadableFile;
006import ro.fortsoft.pf4j.PluginWrapper;
007
008import java.io.*;
009import java.lang.ref.WeakReference;
010import java.util.*;
011import java.util.function.Consumer;
012
013/**
014 * Manages property files, and is also a {@link ReloadableFile}
015 *
016 * <p>Unlike most manager classes in Izou, the PropertiesManager is included in every {@code AddOn} instance</p>
017 */
018public class PropertiesAssistant extends AddOnModule implements ReloadableFile {
019    private String propertiesPath;
020    private String defaultPropertiesPath;
021    private Properties properties;
022    private final EventPropertiesAssistant assistant;
023    private File propertiesFile;
024    private List<WeakReference<Consumer<PropertiesAssistant>>> listeners =
025            Collections.synchronizedList(new ArrayList<>());
026
027    public PropertiesAssistant(Context context, String addOnID) {
028        super(context, addOnID + ".PropertiesAssistant");
029        this.properties = new Properties();
030        this.propertiesPath = null;
031        this.defaultPropertiesPath = null;
032        this.assistant = new EventPropertiesAssistant(context, addOnID + ".EventPropertiesAssistant");
033        PluginWrapper plugin = getContext().getAddOn().getPlugin();
034        if (plugin != null) {
035            this.defaultPropertiesPath = getContext().getFiles().getLibLocation() +
036                    getContext().getAddOn().getPlugin().getPluginPath() + File.separator + "classes" + File.separator
037                    + "default_properties.txt";
038        } else {
039            //if we are debugging
040            this.defaultPropertiesPath = getContext().getAddOn().getClass().getClassLoader().
041                    getResource("default_properties.txt").getFile();
042        }
043        initProperties();
044        try {
045            getContext().getFiles().registerFileDir(propertiesFile.getParentFile().toPath(),
046                    propertiesFile.getName(), this);
047        } catch (IOException e) {
048            error("Error registering reloadablefile with file manager", e);
049        }
050    }
051
052    /**
053     * Gets the EventPropertiesAssistant
054     *
055     * @return the EventPropertiesAssistant
056     */
057    public EventPropertiesAssistant getEventPropertiesAssistant() {
058        return assistant;
059    }
060
061    /**
062     * Searches for the property with the specified key in this property list.
063     *
064     * If the key is not found in this property list, the default property list, and its defaults, recursively, are
065     * then checked. The method returns null if the property is not found.
066     *
067     * @param key the property key.
068     * @return the value in this property list with the specified key value.
069     */
070    public String getProperty(String key) {
071        return properties.getProperty(key);
072    }
073
074    /**
075     * Gets the properties object
076     *
077     * @return the properties object
078     */
079    public Properties getProperties() {
080        return properties;
081    }
082
083    /**
084     * Calls the HashTable method put.
085     *
086     * Provided for parallelism with the getProperty method. Enforces use of strings for
087     *     * property keys and values. The value returned is the result of the HashTable call to put.
088
089     * @param key the key to be placed into this property list.
090     * @param value the value corresponding to key.
091     */
092    public void setProperty(String key, String value) {
093        properties.setProperty(key, value);
094    }
095
096    /**
097     * Gets the path to properties file (the real properties file - as opposed to the {@code defaultProperties.txt} file)
098     *
099     * @return path to properties file
100     */
101    public String getPropertiesPath() {
102        return propertiesPath;
103    }
104
105    /**
106     * Gets the path to properties file (the real properties file - as opposed to the {@code defaultProperties.txt} file)
107     *
108     * @return path to properties file
109     */
110    public File getPropertiesFile() {
111        return propertiesFile;
112    }
113
114    /**
115     * the listener will always be called, when the Properties-file changes.
116     *
117     * @param listener the listener
118     */
119    public void registerUpdateListener(Consumer<PropertiesAssistant> listener) {
120        if (listener != null)
121            listeners.add(new WeakReference<>(listener));
122    }
123
124    /**
125     * Gets the path to default properties file path (the file which is copied into the real properties on start)
126     *
127     * @return path to default properties file
128     */
129    public String getDefaultPropertiesPath() {
130        return defaultPropertiesPath;
131    }
132
133    /**
134     * Initializes properties in the addOn. Creates new properties file using default properties.
135     */
136    public void initProperties() {
137        propertiesPath = getContext().getFiles().getPropertiesLocation() + File.separator
138                + getContext().getAddOn().getID() + ".properties";
139
140        this.propertiesFile = new File(propertiesPath);
141        if (!this.propertiesFile.exists()) try {
142            this.propertiesFile.createNewFile();
143        } catch (IOException e) {
144            error("Error while trying to create the new Properties file", e);
145        }
146
147        try {
148            BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(this.propertiesFile),
149                    "UTF8"));
150            try {
151                properties.load(in);
152            } catch (IOException e) {
153                error("unable to load the InputStream for the PropertiesFile",e);
154            }
155        } catch (FileNotFoundException | UnsupportedEncodingException e) {
156            error("Error while trying to read Properties-File", e);
157        }
158
159        if (defaultPropertiesPath != null && new File(defaultPropertiesPath).exists()) {
160            @SuppressWarnings("unchecked")
161            Enumeration<String> keys = (Enumeration<String>)properties.propertyNames();
162
163            if (!keys.hasMoreElements()) {
164                try {
165                    createDefaultPropertyFile(defaultPropertiesPath);
166                } catch (IOException e) {
167                    error("Error while trying to copy the Default-Properties File", e);
168                }
169
170                if (new File(defaultPropertiesPath).exists() && !writeToPropertiesFile(defaultPropertiesPath)) return;
171                reloadProperties();
172            }
173        }
174    }
175
176    /**
177     * Writes defaultPropertiesFile.txt to real properties file
178     * This is done so that the final user never has to worry about property file initialization
179     *
180     * @param defaultPropsPath path to defaultPropertyFile.txt (or where it should be created)
181     * @return true if operation has succeeded, else false
182     */
183    private boolean writeToPropertiesFile(String defaultPropsPath) {
184        return getContext().getFiles().writeToFile(defaultPropsPath, propertiesPath);
185    }
186
187    /**
188     * Creates a defaultPropertyFile.txt in case it does not exist yet. In case it is used by an addOn,
189     * it copies its content into the real properties file every time the addOn is launched.
190     *
191     * It is impossible to get the properties file on default, that way the user should not have to worry about
192     * the property file's initial content.
193     *
194     * @param defaultPropsPath path to defaultPropertyFile.txt (or where it should be created)
195     * @throws java.io.IOException is thrown by bufferedWriter
196     */
197    private void createDefaultPropertyFile(String defaultPropsPath) throws IOException {
198        getContext().getFiles().createDefaultFile(defaultPropsPath, "# Properties should always be in the "
199                + "form of: \"key = value\"");
200    }
201
202    /**
203     * reloads the propertiesFile into the properties object
204     */
205    private void reloadProperties() {
206        Properties temp = new Properties();
207        BufferedReader bufferedReader = null;
208        try {
209            File properties = new File(propertiesPath);
210            bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(properties), "UTF8"));
211            temp.load(bufferedReader);
212            this.properties = temp;
213            listeners.removeIf(weakReference -> weakReference.get() == null);
214            listeners.forEach(weakReference -> {
215                Consumer<PropertiesAssistant> consumer = weakReference.get();
216                if (consumer != null)
217                    consumer.accept(this);
218            });
219        } catch (IOException e) {
220            error("Error while trying to load the Properties-File: "
221                    + propertiesPath, e);
222        } finally {
223            if (bufferedReader != null) {
224                try {
225                    bufferedReader.close();
226                } catch (IOException e) {
227                    error("Unable to close input stream", e);
228                }
229            }
230        }
231    }
232
233    @Override
234    public void reloadFile(String eventType) {
235        reloadProperties();
236    }
237}