1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.configuration;
19
20 import java.io.File;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.OutputStream;
26 import java.io.OutputStreamWriter;
27 import java.io.Reader;
28 import java.io.UnsupportedEncodingException;
29 import java.io.Writer;
30 import java.net.MalformedURLException;
31 import java.net.URL;
32 import java.util.Iterator;
33
34 import org.apache.commons.configuration.reloading.InvariantReloadingStrategy;
35 import org.apache.commons.configuration.reloading.ReloadingStrategy;
36 import org.apache.commons.lang.StringUtils;
37 import org.apache.commons.logging.LogFactory;
38
39 /***
40 * <p>Partial implementation of the <code>FileConfiguration</code> interface.
41 * Developpers of file based configuration may want to extend this class,
42 * the two methods left to implement are <code>{@link FileConfiguration#load(Reader)}</code>
43 * and <code>{@link FileConfiguration#save(Writer)}</code>.</p>
44 * <p>This base class already implements a couple of ways to specify the location
45 * of the file this configuration is based on. The following possibilities
46 * exist:
47 * <ul><li>URLs: With the method <code>setURL()</code> a full URL to the
48 * configuration source can be specified. This is the most flexible way. Note
49 * that the <code>save()</code> methods support only <em>file:</em> URLs.</li>
50 * <li>Files: The <code>setFile()</code> method allows to specify the
51 * configuration source as a file. This can be either a relative or an
52 * absolute file. In the former case the file is resolved based on the current
53 * directory.</li>
54 * <li>As file paths in string form: With the <code>setPath()</code> method a
55 * full path to a configuration file can be provided as a string.</li>
56 * <li>Separated as base path and file name: This is the native form in which
57 * the location is stored. The base path is a string defining either a local
58 * directory or a URL. It can be set using the <code>setBasePath()</code>
59 * method. The file name, non surprisingly, defines the name of the configuration
60 * file.</li></ul></p>
61 * <p>Note that the <code>load()</code> methods do not wipe out the configuration's
62 * content before the new configuration file is loaded. Thus it is very easy to
63 * construct a union configuration by simply loading multiple configuration
64 * files, e.g.</p>
65 * <p><pre>
66 * config.load(configFile1);
67 * config.load(configFile2);
68 * </pre></p>
69 * <p>After executing this code fragment, the resulting configuration will
70 * contain both the properties of configFile1 and configFile2. On the other
71 * hand, if the current configuration file is to be reloaded, <code>clear()</code>
72 * should be called first. Otherwise the properties are doubled. This behavior
73 * is analogous to the behavior of the <code>load(InputStream)</code> method
74 * in <code>java.util.Properties</code>.</p>
75 *
76 * @author Emmanuel Bourg
77 * @version $Revision: 497574 $, $Date: 2007-01-18 22:02:55 +0100 (Do, 18 Jan 2007) $
78 * @since 1.0-rc2
79 */
80 public abstract class AbstractFileConfiguration extends BaseConfiguration implements FileConfiguration
81 {
82 /*** Constant for the configuration reload event.*/
83 public static final int EVENT_RELOAD = 20;
84
85 /*** Stores the file name.*/
86 protected String fileName;
87
88 /*** Stores the base path.*/
89 protected String basePath;
90
91 /*** The auto save flag.*/
92 protected boolean autoSave;
93
94 /*** Holds a reference to the reloading strategy.*/
95 protected ReloadingStrategy strategy;
96
97 /*** A lock object for protecting reload operations.*/
98 private Object reloadLock = new Object();
99
100 /*** Stores the encoding of the configuration file.*/
101 private String encoding;
102
103 /*** Stores the URL from which the configuration file was loaded.*/
104 private URL sourceURL;
105
106 /*** A counter that prohibits reloading.*/
107 private int noReload;
108
109 /***
110 * Default constructor
111 *
112 * @since 1.1
113 */
114 public AbstractFileConfiguration()
115 {
116 initReloadingStrategy();
117 setLogger(LogFactory.getLog(getClass()));
118 addErrorLogListener();
119 }
120
121 /***
122 * Creates and loads the configuration from the specified file. The passed
123 * in string must be a valid file name, either absolute or relativ.
124 *
125 * @param fileName The name of the file to load.
126 *
127 * @throws ConfigurationException Error while loading the file
128 * @since 1.1
129 */
130 public AbstractFileConfiguration(String fileName) throws ConfigurationException
131 {
132 this();
133
134
135 setFileName(fileName);
136
137
138 load();
139 }
140
141 /***
142 * Creates and loads the configuration from the specified file.
143 *
144 * @param file The file to load.
145 * @throws ConfigurationException Error while loading the file
146 * @since 1.1
147 */
148 public AbstractFileConfiguration(File file) throws ConfigurationException
149 {
150 this();
151
152
153 setFile(file);
154
155
156 if (file.exists())
157 {
158 load();
159 }
160 }
161
162 /***
163 * Creates and loads the configuration from the specified URL.
164 *
165 * @param url The location of the file to load.
166 * @throws ConfigurationException Error while loading the file
167 * @since 1.1
168 */
169 public AbstractFileConfiguration(URL url) throws ConfigurationException
170 {
171 this();
172
173
174 setURL(url);
175
176
177 load();
178 }
179
180 /***
181 * Load the configuration from the underlying location.
182 *
183 * @throws ConfigurationException if loading of the configuration fails
184 */
185 public void load() throws ConfigurationException
186 {
187 if (sourceURL != null)
188 {
189 load(sourceURL);
190 }
191 else
192 {
193 load(getFileName());
194 }
195 }
196
197 /***
198 * Locate the specified file and load the configuration. This does not
199 * change the source of the configuration (i.e. the internally maintained file name).
200 * Use one of the setter methods for this purpose.
201 *
202 * @param fileName the name of the file to be loaded
203 * @throws ConfigurationException if an error occurs
204 */
205 public void load(String fileName) throws ConfigurationException
206 {
207 try
208 {
209 URL url = ConfigurationUtils.locate(basePath, fileName);
210
211 if (url == null)
212 {
213 throw new ConfigurationException("Cannot locate configuration source " + fileName);
214 }
215 load(url);
216 }
217 catch (ConfigurationException e)
218 {
219 throw e;
220 }
221 catch (Exception e)
222 {
223 throw new ConfigurationException(e.getMessage(), e);
224 }
225 }
226
227 /***
228 * Load the configuration from the specified file. This does not change
229 * the source of the configuration (i.e. the internally maintained file
230 * name). Use one of the setter methods for this purpose.
231 *
232 * @param file the file to load
233 * @throws ConfigurationException if an error occurs
234 */
235 public void load(File file) throws ConfigurationException
236 {
237 try
238 {
239 load(file.toURL());
240 }
241 catch (ConfigurationException e)
242 {
243 throw e;
244 }
245 catch (Exception e)
246 {
247 throw new ConfigurationException(e.getMessage(), e);
248 }
249 }
250
251 /***
252 * Load the configuration from the specified URL. This does not change the
253 * source of the configuration (i.e. the internally maintained file name).
254 * Use on of the setter methods for this purpose.
255 *
256 * @param url the URL of the file to be loaded
257 * @throws ConfigurationException if an error occurs
258 */
259 public void load(URL url) throws ConfigurationException
260 {
261 if (sourceURL == null)
262 {
263 if (StringUtils.isEmpty(getBasePath()))
264 {
265
266 setBasePath(url.toString());
267 }
268 sourceURL = url;
269 }
270
271
272 File file = ConfigurationUtils.fileFromURL(url);
273 if (file != null && file.isDirectory())
274 {
275 throw new ConfigurationException("Cannot load a configuration from a directory");
276 }
277
278 InputStream in = null;
279
280 try
281 {
282 in = url.openStream();
283 load(in);
284 }
285 catch (ConfigurationException e)
286 {
287 throw e;
288 }
289 catch (Exception e)
290 {
291 throw new ConfigurationException(e.getMessage(), e);
292 }
293 finally
294 {
295
296 try
297 {
298 if (in != null)
299 {
300 in.close();
301 }
302 }
303 catch (IOException e)
304 {
305 getLogger().warn("Could not close input stream", e);
306 }
307 }
308 }
309
310 /***
311 * Load the configuration from the specified stream, using the encoding
312 * returned by {@link #getEncoding()}.
313 *
314 * @param in the input stream
315 *
316 * @throws ConfigurationException if an error occurs during the load operation
317 */
318 public void load(InputStream in) throws ConfigurationException
319 {
320 load(in, getEncoding());
321 }
322
323 /***
324 * Load the configuration from the specified stream, using the specified
325 * encoding. If the encoding is null the default encoding is used.
326 *
327 * @param in the input stream
328 * @param encoding the encoding used. <code>null</code> to use the default encoding
329 *
330 * @throws ConfigurationException if an error occurs during the load operation
331 */
332 public void load(InputStream in, String encoding) throws ConfigurationException
333 {
334 Reader reader = null;
335
336 if (encoding != null)
337 {
338 try
339 {
340 reader = new InputStreamReader(in, encoding);
341 }
342 catch (UnsupportedEncodingException e)
343 {
344 throw new ConfigurationException(
345 "The requested encoding is not supported, try the default encoding.", e);
346 }
347 }
348
349 if (reader == null)
350 {
351 reader = new InputStreamReader(in);
352 }
353
354 load(reader);
355 }
356
357 /***
358 * Save the configuration. Before this method can be called a valid file
359 * name must have been set.
360 *
361 * @throws ConfigurationException if an error occurs or no file name has
362 * been set yet
363 */
364 public void save() throws ConfigurationException
365 {
366 if (getFileName() == null)
367 {
368 throw new ConfigurationException("No file name has been set!");
369 }
370
371 if (sourceURL != null)
372 {
373 save(sourceURL);
374 }
375 else
376 {
377 save(fileName);
378 }
379 strategy.init();
380 }
381
382 /***
383 * Save the configuration to the specified file. This doesn't change the
384 * source of the configuration, use setFileName() if you need it.
385 *
386 * @param fileName the file name
387 *
388 * @throws ConfigurationException if an error occurs during the save operation
389 */
390 public void save(String fileName) throws ConfigurationException
391 {
392 try
393 {
394 File file = ConfigurationUtils.getFile(basePath, fileName);
395 if (file == null)
396 {
397 throw new ConfigurationException("Invalid file name for save: " + fileName);
398 }
399 save(file);
400 }
401 catch (ConfigurationException e)
402 {
403 throw e;
404 }
405 catch (Exception e)
406 {
407 throw new ConfigurationException(e.getMessage(), e);
408 }
409 }
410
411 /***
412 * Save the configuration to the specified URL if it's a file URL.
413 * This doesn't change the source of the configuration, use setURL()
414 * if you need it.
415 *
416 * @param url the URL
417 *
418 * @throws ConfigurationException if an error occurs during the save operation
419 */
420 public void save(URL url) throws ConfigurationException
421 {
422 File file = ConfigurationUtils.fileFromURL(url);
423 if (file != null)
424 {
425 save(file);
426 }
427 else
428 {
429 throw new ConfigurationException("Could not save to URL " + url);
430 }
431 }
432
433 /***
434 * Save the configuration to the specified file. The file is created
435 * automatically if it doesn't exist. This doesn't change the source
436 * of the configuration, use {@link #setFile} if you need it.
437 *
438 * @param file the target file
439 *
440 * @throws ConfigurationException if an error occurs during the save operation
441 */
442 public void save(File file) throws ConfigurationException
443 {
444 OutputStream out = null;
445
446 try
447 {
448
449 createPath(file);
450 out = new FileOutputStream(file);
451 save(out);
452 }
453 catch (IOException e)
454 {
455 throw new ConfigurationException(e.getMessage(), e);
456 }
457 finally
458 {
459
460 try
461 {
462 if (out != null)
463 {
464 out.close();
465 }
466 }
467 catch (IOException e)
468 {
469 getLogger().warn("Could not close output stream", e);
470 }
471 }
472 }
473
474 /***
475 * Save the configuration to the specified stream, using the encoding
476 * returned by {@link #getEncoding()}.
477 *
478 * @param out the output stream
479 *
480 * @throws ConfigurationException if an error occurs during the save operation
481 */
482 public void save(OutputStream out) throws ConfigurationException
483 {
484 save(out, getEncoding());
485 }
486
487 /***
488 * Save the configuration to the specified stream, using the specified
489 * encoding. If the encoding is null the default encoding is used.
490 *
491 * @param out the output stream
492 * @param encoding the encoding to use
493 * @throws ConfigurationException if an error occurs during the save operation
494 */
495 public void save(OutputStream out, String encoding) throws ConfigurationException
496 {
497 Writer writer = null;
498
499 if (encoding != null)
500 {
501 try
502 {
503 writer = new OutputStreamWriter(out, encoding);
504 }
505 catch (UnsupportedEncodingException e)
506 {
507 throw new ConfigurationException(
508 "The requested encoding is not supported, try the default encoding.", e);
509 }
510 }
511
512 if (writer == null)
513 {
514 writer = new OutputStreamWriter(out);
515 }
516
517 save(writer);
518 }
519
520 /***
521 * Return the name of the file.
522 *
523 * @return the file name
524 */
525 public String getFileName()
526 {
527 return fileName;
528 }
529
530 /***
531 * Set the name of the file. The passed in file name can contain a
532 * relative path.
533 * It must be used when referring files with relative paths from classpath.
534 * Use <code>{@link AbstractFileConfiguration#setPath(String)
535 * setPath()}</code> to set a full qualified file name.
536 *
537 * @param fileName the name of the file
538 */
539 public void setFileName(String fileName)
540 {
541 sourceURL = null;
542 this.fileName = fileName;
543 }
544
545 /***
546 * Return the base path.
547 *
548 * @return the base path
549 * @see FileConfiguration#getBasePath()
550 */
551 public String getBasePath()
552 {
553 return basePath;
554 }
555
556 /***
557 * Sets the base path. The base path is typically either a path to a
558 * directory or a URL. Together with the value passed to the
559 * <code>setFileName()</code> method it defines the location of the
560 * configuration file to be loaded. The strategies for locating the file are
561 * quite tolerant. For instance if the file name is already an absolute path
562 * or a fully defined URL, the base path will be ignored. The base path can
563 * also be a URL, in which case the file name is interpreted in this URL's
564 * context. Because the base path is used by some of the derived classes for
565 * resolving relative file names it should contain a meaningful value. If
566 * other methods are used for determining the location of the configuration
567 * file (e.g. <code>setFile()</code> or <code>setURL()</code>), the
568 * base path is automatically set.
569 *
570 * @param basePath the base path.
571 */
572 public void setBasePath(String basePath)
573 {
574 sourceURL = null;
575 this.basePath = basePath;
576 }
577
578 /***
579 * Return the file where the configuration is stored. If the base path is a
580 * URL with a protocol different than "file", or the configuration
581 * file is within a compressed archive, the return value
582 * will not point to a valid file object.
583 *
584 * @return the file where the configuration is stored; this can be <b>null</b>
585 */
586 public File getFile()
587 {
588 if (getFileName() == null)
589 {
590 return null;
591 }
592 else
593 {
594 if (sourceURL != null)
595 {
596 return ConfigurationUtils.fileFromURL(sourceURL);
597 }
598 else
599 {
600 return ConfigurationUtils.getFile(getBasePath(), getFileName());
601 }
602 }
603 }
604
605 /***
606 * Set the file where the configuration is stored. The passed in file is
607 * made absolute if it is not yet. Then the file's path component becomes
608 * the base path and its name component becomes the file name.
609 *
610 * @param file the file where the configuration is stored
611 */
612 public void setFile(File file)
613 {
614 sourceURL = null;
615 setFileName(file.getName());
616 setBasePath((file.getParentFile() != null) ? file.getParentFile()
617 .getAbsolutePath() : null);
618 }
619
620 /***
621 * Returns the full path to the file this configuration is based on. The
622 * return value is a valid File path only if this configuration is based on
623 * a file on the local disk.
624 * If the configuration was loaded from a packed archive the returned value
625 * is the string form of the URL from which the configuration was loaded.
626 *
627 * @return the full path to the configuration file
628 */
629 public String getPath()
630 {
631 String path = null;
632 File file = getFile();
633
634 if (file != null)
635 {
636 path = file.getAbsolutePath();
637 }
638
639
640 if (path == null)
641 {
642 if (sourceURL != null)
643 {
644 path = sourceURL.getPath();
645 }
646 else
647 {
648 try
649 {
650 path = ConfigurationUtils.getURL(getBasePath(),
651 getFileName()).getPath();
652 }
653 catch (MalformedURLException e)
654 {
655
656 ;
657 }
658 }
659 }
660
661 return path;
662 }
663
664 /***
665 * Sets the location of this configuration as a full or relative path name.
666 * The passed in path should represent a valid file name on the file system.
667 * It must not be used to specify relative paths for files that exist
668 * in classpath, either plain file system or compressed archive,
669 * because this method expands any relative path to an absolute one which
670 * may end in an invalid absolute path for classpath references.
671 *
672 * @param path the full path name of the configuration file
673 */
674 public void setPath(String path)
675 {
676 setFile(new File(path));
677 }
678
679 /***
680 * Return the URL where the configuration is stored.
681 *
682 * @return the configuration's location as URL
683 */
684 public URL getURL()
685 {
686 return (sourceURL != null) ? sourceURL
687 : ConfigurationUtils.locate(getBasePath(), getFileName());
688 }
689
690 /***
691 * Set the location of this configuration as a URL. For loading this can be
692 * an arbitrary URL with a supported protocol. If the configuration is to
693 * be saved, too, a URL with the "file" protocol should be
694 * provided.
695 *
696 * @param url the location of this configuration as URL
697 */
698 public void setURL(URL url)
699 {
700 setBasePath(ConfigurationUtils.getBasePath(url));
701 setFileName(ConfigurationUtils.getFileName(url));
702 sourceURL = url;
703 }
704
705 public void setAutoSave(boolean autoSave)
706 {
707 this.autoSave = autoSave;
708 }
709
710 public boolean isAutoSave()
711 {
712 return autoSave;
713 }
714
715 /***
716 * Save the configuration if the automatic persistence is enabled
717 * and if a file is specified.
718 */
719 protected void possiblySave()
720 {
721 if (autoSave && fileName != null)
722 {
723 try
724 {
725 save();
726 }
727 catch (ConfigurationException e)
728 {
729 throw new ConfigurationRuntimeException("Failed to auto-save", e);
730 }
731 }
732 }
733
734 /***
735 * Adds a new property to this configuration. This implementation checks if
736 * the auto save mode is enabled and saves the configuration if necessary.
737 *
738 * @param key the key of the new property
739 * @param value the value
740 */
741 public void addProperty(String key, Object value)
742 {
743 super.addProperty(key, value);
744 possiblySave();
745 }
746
747 /***
748 * Sets a new value for the specified property. This implementation checks
749 * if the auto save mode is enabled and saves the configuration if
750 * necessary.
751 *
752 * @param key the key of the affected property
753 * @param value the value
754 */
755 public void setProperty(String key, Object value)
756 {
757 super.setProperty(key, value);
758 possiblySave();
759 }
760
761 public void clearProperty(String key)
762 {
763 super.clearProperty(key);
764 possiblySave();
765 }
766
767 public ReloadingStrategy getReloadingStrategy()
768 {
769 return strategy;
770 }
771
772 public void setReloadingStrategy(ReloadingStrategy strategy)
773 {
774 this.strategy = strategy;
775 strategy.setConfiguration(this);
776 strategy.init();
777 }
778
779 /***
780 * Performs a reload operation if necessary. This method is called on each
781 * access of this configuration. It asks the associated reloading strategy
782 * whether a reload should be performed. If this is the case, the
783 * configuration is cleared and loaded again from its source. If this
784 * operation causes an exception, the registered error listeners will be
785 * notified. The error event passed to the listeners is of type
786 * <code>EVENT_RELOAD</code> and contains the exception that caused the
787 * event.
788 */
789 public void reload()
790 {
791 synchronized (reloadLock)
792 {
793 if (noReload == 0)
794 {
795 try
796 {
797 enterNoReload();
798
799 if (strategy.reloadingRequired())
800 {
801 if (getLogger().isInfoEnabled())
802 {
803 getLogger().info("Reloading configuration. URL is " + getURL());
804 }
805 fireEvent(EVENT_RELOAD, null, getURL(), true);
806 setDetailEvents(false);
807 try
808 {
809 clear();
810 load();
811 }
812 finally
813 {
814 setDetailEvents(true);
815 }
816 fireEvent(EVENT_RELOAD, null, getURL(), false);
817
818
819 strategy.reloadingPerformed();
820 }
821 }
822 catch (Exception e)
823 {
824 fireError(EVENT_RELOAD, null, null, e);
825
826 }
827 finally
828 {
829 exitNoReload();
830 }
831 }
832 }
833 }
834
835 /***
836 * Enters the "No reloading mode". As long as this mode is active
837 * no reloading will be performed. This is necessary for some
838 * implementations of <code>save()</code> in derived classes, which may
839 * cause a reload while accessing the properties to save. This may cause the
840 * whole configuration to be erased. To avoid this, this method can be
841 * called first. After a call to this method there always must be a
842 * corresponding call of <code>{@link #exitNoReload()}</code> later! (If
843 * necessary, <code>finally</code> blocks must be used to ensure this.
844 */
845 protected void enterNoReload()
846 {
847 synchronized (reloadLock)
848 {
849 noReload++;
850 }
851 }
852
853 /***
854 * Leaves the "No reloading mode".
855 *
856 * @see #enterNoReload()
857 */
858 protected void exitNoReload()
859 {
860 synchronized (reloadLock)
861 {
862 if (noReload > 0)
863 {
864 noReload--;
865 }
866 }
867 }
868
869 /***
870 * Sends an event to all registered listeners. This implementation ensures
871 * that no reloads are performed while the listeners are invoked. So
872 * infinite loops can be avoided that can be caused by event listeners
873 * accessing the configuration's properties when they are invoked.
874 *
875 * @param type the event type
876 * @param propName the name of the property
877 * @param propValue the value of the property
878 * @param before the before update flag
879 */
880 protected void fireEvent(int type, String propName, Object propValue,
881 boolean before)
882 {
883 enterNoReload();
884 try
885 {
886 super.fireEvent(type, propName, propValue, before);
887 }
888 finally
889 {
890 exitNoReload();
891 }
892 }
893
894 public Object getProperty(String key)
895 {
896 reload();
897 return super.getProperty(key);
898 }
899
900 public boolean isEmpty()
901 {
902 reload();
903 return super.isEmpty();
904 }
905
906 public boolean containsKey(String key)
907 {
908 reload();
909 return super.containsKey(key);
910 }
911
912 public Iterator getKeys()
913 {
914 reload();
915 return super.getKeys();
916 }
917
918 /***
919 * Create the path to the specified file.
920 *
921 * @param file the target file
922 */
923 private void createPath(File file)
924 {
925 if (file != null)
926 {
927
928 if (!file.exists())
929 {
930 File parent = file.getParentFile();
931 if (parent != null && !parent.exists())
932 {
933 parent.mkdirs();
934 }
935 }
936 }
937 }
938
939 public String getEncoding()
940 {
941 return encoding;
942 }
943
944 public void setEncoding(String encoding)
945 {
946 this.encoding = encoding;
947 }
948
949 /***
950 * Creates a copy of this configuration. The new configuration object will
951 * contain the same properties as the original, but it will lose any
952 * connection to a source file (if one exists); this includes setting the
953 * source URL, base path, and file name to <b>null</b>. This is done to
954 * avoid race conditions if both the original and the copy are modified and
955 * then saved.
956 *
957 * @return the copy
958 * @since 1.3
959 */
960 public Object clone()
961 {
962 AbstractFileConfiguration copy = (AbstractFileConfiguration) super
963 .clone();
964 copy.setBasePath(null);
965 copy.setFileName(null);
966 copy.initReloadingStrategy();
967 return copy;
968 }
969
970 /***
971 * Helper method for initializing the reloading strategy.
972 */
973 private void initReloadingStrategy()
974 {
975 setReloadingStrategy(new InvariantReloadingStrategy());
976 }
977 }