View Javadoc

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