1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package no.feide.moria.configuration;
22
23 import no.feide.moria.controller.MoriaController;
24 import no.feide.moria.log.MessageLogger;
25
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.FileNotFoundException;
29 import java.io.IOException;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.Properties;
33 import java.util.Timer;
34 import java.util.TimerTask;
35
36 /***
37 * The configuration manager's task is to load and monitor the configuration
38 * files for changes. Each module (authorization, web, store and directory)
39 * has a configuration file which is read and passed as a Properties object
40 * to the module at startup. The authorization module does its own file
41 * parsing, so for that module the Properties object just contains the
42 * name of the configuration file, which is then read and parsed by the
43 * authorization module.
44 *
45 * If a configuration file is changed, the entire file is reread and the
46 * updated configuration is passed to the corresponding module.
47 *
48 * The constructor requires the <code>no.feide.moria.configuration.cm</code>
49 * property to be set, and the property has to point to the configuration file
50 * for the ConfigurationManager module. The file can be referenced by either
51 * full file path or as a resource in the classpath. <br/><br/>The
52 * configuration file has to contain properties that point to the other modules'
53 * properties files. These files can be referenced by either full file path or
54 * as a resource in the classpath. The
55 * <code>no.feide.moria.configuration.fileListenerIntervalSeconds</code>
56 * attribute specifies the interval between each file poll. <br/><p/>
57 *
58 * <pre>
59 *
60 *
61 *
62 *
63 *
64 *
65 * # Example content for ConfigurationManager properties
66 * no.feide.moria.configuration.fileListenerIntervalSeconds=1
67 * no.feide.moria.configuration.sm=/sm-test-valid.properties
68 * no.feide.moria.configuration.dm=/dm-test-valid.properties
69 * no.feide.moria.configuration.am=/am-data.xml
70 *
71 *
72 *
73 *
74 *
75 *
76 * </pre>
77 *
78 * <p/>When a configuration file is changed, the content is read into a
79 * properties object which is sent to the MoriaController.
80 *
81 * @author Lars Preben S. Arnesen <lars.preben.arnesen@conduct.no>
82 * @version $Revision: 1.24 $
83 * @see no.feide.moria.controller.MoriaController
84 */
85 public final class ConfigurationManager {
86
87 /***
88 * For logging events that do not throw exceptions to the layer above.
89 */
90 private final MessageLogger messageLogger = new MessageLogger(ConfigurationManager.class);
91
92 /***
93 * Name of the Store module, used in configuration properties.
94 */
95 public static final String MODULE_SM = "sm";
96
97 /***
98 * Name of the Directory module, used in configuration properties.
99 */
100 public static final String MODULE_DM = "dm";
101
102 /***
103 * Name of the Authorization module, used in configuration properties.
104 */
105 public static final String MODULE_AM = "am";
106
107 /***
108 * Name of the Configuration module, used in configuration properties.
109 */
110 private static final String MODULE_CM = "base";
111
112 /***
113 * Name of the Web module, used in configuration properties.
114 */
115 public static final String MODULE_WEB = "web";
116
117 /***
118 * Attribute name for timer delay.
119 */
120 private static final String TIMER_DELAY = "fileListenerIntervalSeconds";
121
122 /***
123 * Attribute name prefix for file name properties.
124 */
125 private static final String PROPS_PREFIX = "no.feide.moria.configuration.";
126
127 /***
128 * List of the modules that have configuration to watch.
129 */
130 private static final String[] NEEDS_LISTENER = new String[] {MODULE_SM, MODULE_DM, MODULE_AM, MODULE_WEB};
131
132 /***
133 * Timer for the configuration files.
134 */
135 private final Timer timer = new Timer(true);
136
137 /***
138 * Storage for all timers.
139 */
140 private final HashMap timerEntries = new HashMap();
141
142
143 /***
144 * Constructor. The constructor reads the ConfigurationManager's properties
145 * from file (set by <code>System.properties</code>) and starts file
146 * listeners for all modules' configuration files.
147 * @throws BaseConfigException
148 * If the system property pointing to the base configuration
149 * file is not a non-empty string.
150 * @throws ConfigurationManagerException
151 * If there are any problems with the configuration file.
152 */
153 public ConfigurationManager() throws ConfigurationManagerException {
154
155
156 final Properties cmProps;
157 final String cmPropsFile = System.getProperty(PROPS_PREFIX + MODULE_CM);
158 if (cmPropsFile == null || cmPropsFile.equals(""))
159 throw new BaseConfigException("System property '" + PROPS_PREFIX + MODULE_CM + "' must be a non-empty string");
160 final String filePrefix = new File(cmPropsFile).getParent() + File.separator;
161 try {
162 cmProps = readProperties(cmPropsFile);
163 } catch (FileNotFoundException e) {
164 throw new ConfigurationManagerException("Configuration manager's configuration file not found: " + cmPropsFile);
165 } catch (IOException e) {
166 throw new ConfigurationManagerException("IOException while loading configuration manager's properties file: " + cmPropsFile, e);
167 }
168
169
170 final int timerDelay;
171 final String timerDelayStr = cmProps.getProperty(PROPS_PREFIX + TIMER_DELAY);
172 if (timerDelayStr == null || timerDelayStr.equals("")) {
173 throw new ConfigurationManagerException("'" + PROPS_PREFIX + TIMER_DELAY
174 + "' in configuration manager properties cannot be a null value.");
175 }
176 timerDelay = new Integer(timerDelayStr).intValue();
177 if (timerDelay < 1) {
178 throw new ConfigurationManagerException("'" + PROPS_PREFIX + TIMER_DELAY
179 + "' in configuration manager properties must be >= 1.");
180 }
181
182
183 for (int i = 0; i < NEEDS_LISTENER.length; i++) {
184 final String module = NEEDS_LISTENER[i];
185 String fileName = cmProps.getProperty(PROPS_PREFIX + module);
186
187
188 if (!fileName.equals(new File(fileName).getAbsolutePath())) {
189 fileName = filePrefix + fileName;
190 }
191
192
193 try {
194 addFileChangeListener(fileName, module, timerDelay);
195 } catch (FileNotFoundException e) {
196 throw new ConfigurationManagerException("Unable to watch file, file not found: " + fileName);
197 }
198 }
199 }
200
201
202 /***
203 * Remove all file listeners.
204 */
205 public void stop() {
206
207 final HashMap timers = new HashMap(timerEntries);
208 for (Iterator it = timers.keySet().iterator(); it.hasNext();) {
209 final String entry = (String) it.next();
210 removeFileChangeListener(entry);
211 }
212
213 timer.cancel();
214 }
215
216
217 /***
218 * Destructor. Will call <code>stop()</code>.
219 * @see ConfigurationManager#stop()
220 */
221 public void destroy() {
222
223 stop();
224 }
225
226
227 /***
228 * Number of active file listeners. Basically needed for testing.
229 * @return The number of active file listeners.
230 */
231 int numFileListeners() {
232
233 return timerEntries.size();
234 }
235
236
237 /***
238 * Read properties from file. The fileURI can be absolute path to file or
239 * relative to the classpath. If the fileURI does not resolve to a readeble
240 * file, an<code>IOException</code> or
241 * <code>IllegalArgumentException</code>is thrown.
242 * @param fileURI
243 * The reference to the properties file.
244 * @return Properties from the file.
245 * @throws IOException
246 * If something goes wrong during file read.
247 * @throws IllegalArgumentException
248 * If <code>fileURI</code> is not a non-empty string.
249 */
250 private static Properties readProperties(final String fileURI)
251 throws IOException {
252
253 final Properties props = new Properties();
254 final File file;
255
256
257 if (fileURI == null || fileURI.equals("")) { throw new IllegalArgumentException("URI to properties file must be a non-empty string."); }
258
259
260 file = fileForURI(fileURI);
261 props.load(new FileInputStream(file));
262
263 return props;
264 }
265
266
267 /***
268 * Monitor a file. A new file listener is started for the module's properties
269 * file. If the file cannot be read, a <code>FileNotFoundException</code>
270 * is thrown.
271 * @param fileName
272 * Full path or relative (classpath) path to the properties file.
273 * @param module
274 * The module the configuration file belongs to.
275 * @param intervalSec
276 * Polling period in seconds.
277 * @throws FileNotFoundException
278 * If the file is not found.
279 */
280 private void addFileChangeListener(final String fileName, final String module, final int intervalSec)
281 throws FileNotFoundException {
282
283 removeFileChangeListener(fileName);
284 final long delay = intervalSec * 1000;
285
286 final FileListenerTask task = new FileListenerTask(fileName, module);
287 timerEntries.put(fileName, task);
288 timer.schedule(task, delay, delay);
289 }
290
291
292 /***
293 * Stop monitoring file.
294 * @param fileName
295 * The file name to stop monitoring.
296 */
297 private void removeFileChangeListener(final String fileName) {
298
299 final FileListenerTask task = (FileListenerTask) timerEntries.remove(fileName);
300 if (task != null) {
301 task.cancel();
302 }
303 }
304
305
306 /***
307 * Send changed configuration to <code>MoriaController</code>.
308 * @param module
309 * The module the configuration file belongs to.
310 * @param configurationFile
311 * A <code>File</code> object representing the changed file.
312 * @see no.feide.moria.controller.MoriaController#setConfig
313 */
314 private void fileChangeEvent(final String module, final File configurationFile) {
315
316 Properties props;
317
318 try {
319
320
321 if (module.equals(MODULE_AM)) {
322 props = new Properties();
323 props.put("authorizationDatabase", configurationFile.getAbsolutePath());
324 } else {
325
326 props = readProperties(configurationFile.getAbsolutePath());
327 }
328
329 } catch (FileNotFoundException e) {
330 props = null;
331 messageLogger.logCritical("Watched file disappeared from the file system, fileChangeEvent cancelled. File: "
332 + configurationFile.getAbsolutePath());
333 } catch (IOException e) {
334 props = null;
335 messageLogger.logCritical("IOException during reading of authorization database, fileChangeEvent cancelled. File: "
336 + configurationFile.getAbsolutePath());
337 }
338
339 if (props != null) {
340 MoriaController.setConfig(module, props);
341 } else {
342 messageLogger.logCritical("Unable to create properties from file: " + configurationFile);
343 }
344 }
345
346
347 /***
348 * Resolves a fileURI to a <code>File</code> object.
349 * @param fileURI
350 * Reference to the file (full path or relative within the
351 * classpath).
352 * @return A <code>File</code> object referenced by the fileURI.
353 * @throws FileNotFoundException
354 * If the fileURI cannot be resolved to a readable file.
355 */
356 private static File fileForURI(final String fileURI)
357 throws FileNotFoundException {
358
359 if (fileURI == null || fileURI.equals("")) { throw new FileNotFoundException("File reference cannot be null."); }
360
361 final File file = new File(fileURI);
362 return file;
363 }
364
365 /***
366 * This class is used to monitor the configuration files. An instance of
367 * this class is created for every file to watch. The work is done by the
368 * run() method which is called by the timer.
369 */
370 final class FileListenerTask
371 extends TimerTask {
372
373 /***
374 * The module that the configuration file belongs to.
375 */
376 private final String module;
377
378 /***
379 * The file object representation of the file that is beeing watched.
380 */
381 private final File monitoredFile;
382
383 /***
384 * Last modification of the watched file.
385 */
386 private long lastModified;
387
388
389 /***
390 * Constructor.
391 * @param fileURI
392 * The URI for the file to watch.
393 * @param module
394 * The module the file belongs to.
395 * @throws FileNotFoundException
396 * If the file does not exist.
397 */
398 public FileListenerTask(final String fileURI, final String module)
399 throws FileNotFoundException {
400
401 monitoredFile = fileForURI(fileURI);
402 fileChangeEvent(module, monitoredFile);
403 this.lastModified = 0;
404 this.module = module;
405 this.lastModified = monitoredFile.lastModified();
406 }
407
408
409 /***
410 * Called by the timer. If the file has changed the fileChangeEvent() is
411 * called.
412 * @see ConfigurationManager#fileChangeEvent(String, File)
413 */
414 public void run() {
415
416 final long modified = monitoredFile.lastModified();
417 if (modified != this.lastModified) {
418 this.lastModified = modified;
419 fileChangeEvent(this.module, this.monitoredFile);
420
421 }
422 }
423 }
424 }