View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    *
4    * This program is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU General Public License as published by the Free
6    * Software Foundation; either version 2 of the License, or (at your option)
7    * any later version.
8    *
9    * This program is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12   * more details.
13   *
14   * You should have received a copy of the GNU General Public License along with
15   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16   * Place - Suite 330, Boston, MA 02111-1307, USA.
17   *
18   * $Id: ConfigurationManager.java,v 1.24 2004/12/20 11:35:41 jk Exp $
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 &lt;lars.preben.arnesen@conduct.no&gt;
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         /* Read configuration manager properties file */
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         /* Timer delay */
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         /* Create listener for every module config file */
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             /* Prefix to full path if file path is relative */
188             if (!fileName.equals(new File(fileName).getAbsolutePath())) {
189                 fileName = filePrefix + fileName;
190             }
191 
192             /* Add file listener */
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         /* Validate parameter */
257         if (fileURI == null || fileURI.equals("")) { throw new IllegalArgumentException("URI to properties file must be a non-empty string."); }
258 
259         /* Read properties file */
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             /* Authorization database */
321             if (module.equals(MODULE_AM)) {
322                 props = new Properties();
323                 props.put("authorizationDatabase", configurationFile.getAbsolutePath());
324             } else {
325                 /* Other (normal) configuration files */
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 }