001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.commons.configuration;
019:
020: import java.io.File;
021: import java.io.FileOutputStream;
022: import java.io.IOException;
023: import java.io.InputStream;
024: import java.io.InputStreamReader;
025: import java.io.OutputStream;
026: import java.io.OutputStreamWriter;
027: import java.io.Reader;
028: import java.io.UnsupportedEncodingException;
029: import java.io.Writer;
030: import java.net.MalformedURLException;
031: import java.net.URL;
032: import java.util.Iterator;
033:
034: import org.apache.commons.configuration.reloading.InvariantReloadingStrategy;
035: import org.apache.commons.configuration.reloading.ReloadingStrategy;
036: import org.apache.commons.lang.StringUtils;
037: import org.apache.commons.logging.LogFactory;
038:
039: /**
040: * <p>Partial implementation of the <code>FileConfiguration</code> interface.
041: * Developpers of file based configuration may want to extend this class,
042: * the two methods left to implement are <code>{@link FileConfiguration#load(Reader)}</code>
043: * and <code>{@link FileConfiguration#save(Writer)}</code>.</p>
044: * <p>This base class already implements a couple of ways to specify the location
045: * of the file this configuration is based on. The following possibilities
046: * exist:
047: * <ul><li>URLs: With the method <code>setURL()</code> a full URL to the
048: * configuration source can be specified. This is the most flexible way. Note
049: * that the <code>save()</code> methods support only <em>file:</em> URLs.</li>
050: * <li>Files: The <code>setFile()</code> method allows to specify the
051: * configuration source as a file. This can be either a relative or an
052: * absolute file. In the former case the file is resolved based on the current
053: * directory.</li>
054: * <li>As file paths in string form: With the <code>setPath()</code> method a
055: * full path to a configuration file can be provided as a string.</li>
056: * <li>Separated as base path and file name: This is the native form in which
057: * the location is stored. The base path is a string defining either a local
058: * directory or a URL. It can be set using the <code>setBasePath()</code>
059: * method. The file name, non surprisingly, defines the name of the configuration
060: * file.</li></ul></p>
061: * <p>Note that the <code>load()</code> methods do not wipe out the configuration's
062: * content before the new configuration file is loaded. Thus it is very easy to
063: * construct a union configuration by simply loading multiple configuration
064: * files, e.g.</p>
065: * <p><pre>
066: * config.load(configFile1);
067: * config.load(configFile2);
068: * </pre></p>
069: * <p>After executing this code fragment, the resulting configuration will
070: * contain both the properties of configFile1 and configFile2. On the other
071: * hand, if the current configuration file is to be reloaded, <code>clear()</code>
072: * should be called first. Otherwise the properties are doubled. This behavior
073: * is analogous to the behavior of the <code>load(InputStream)</code> method
074: * in <code>java.util.Properties</code>.</p>
075: *
076: * @author Emmanuel Bourg
077: * @version $Revision: 497574 $, $Date: 2007-01-18 22:02:55 +0100 (Do, 18 Jan 2007) $
078: * @since 1.0-rc2
079: */
080: public abstract class AbstractFileConfiguration extends
081: BaseConfiguration implements FileConfiguration {
082: /** Constant for the configuration reload event.*/
083: public static final int EVENT_RELOAD = 20;
084:
085: /** Stores the file name.*/
086: protected String fileName;
087:
088: /** Stores the base path.*/
089: protected String basePath;
090:
091: /** The auto save flag.*/
092: protected boolean autoSave;
093:
094: /** Holds a reference to the reloading strategy.*/
095: protected ReloadingStrategy strategy;
096:
097: /** A lock object for protecting reload operations.*/
098: private Object reloadLock = new Object();
099:
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: initReloadingStrategy();
116: setLogger(LogFactory.getLog(getClass()));
117: addErrorLogListener();
118: }
119:
120: /**
121: * Creates and loads the configuration from the specified file. The passed
122: * in string must be a valid file name, either absolute or relativ.
123: *
124: * @param fileName The name of the file to load.
125: *
126: * @throws ConfigurationException Error while loading the file
127: * @since 1.1
128: */
129: public AbstractFileConfiguration(String fileName)
130: throws ConfigurationException {
131: this ();
132:
133: // store the file name
134: setFileName(fileName);
135:
136: // load the file
137: load();
138: }
139:
140: /**
141: * Creates and loads the configuration from the specified file.
142: *
143: * @param file The file to load.
144: * @throws ConfigurationException Error while loading the file
145: * @since 1.1
146: */
147: public AbstractFileConfiguration(File file)
148: throws ConfigurationException {
149: this ();
150:
151: // set the file and update the url, the base path and the file name
152: setFile(file);
153:
154: // load the file
155: if (file.exists()) {
156: load();
157: }
158: }
159:
160: /**
161: * Creates and loads the configuration from the specified URL.
162: *
163: * @param url The location of the file to load.
164: * @throws ConfigurationException Error while loading the file
165: * @since 1.1
166: */
167: public AbstractFileConfiguration(URL url)
168: throws ConfigurationException {
169: this ();
170:
171: // set the URL and update the base path and the file name
172: setURL(url);
173:
174: // load the file
175: load();
176: }
177:
178: /**
179: * Load the configuration from the underlying location.
180: *
181: * @throws ConfigurationException if loading of the configuration fails
182: */
183: public void load() throws ConfigurationException {
184: if (sourceURL != null) {
185: load(sourceURL);
186: } else {
187: load(getFileName());
188: }
189: }
190:
191: /**
192: * Locate the specified file and load the configuration. This does not
193: * change the source of the configuration (i.e. the internally maintained file name).
194: * Use one of the setter methods for this purpose.
195: *
196: * @param fileName the name of the file to be loaded
197: * @throws ConfigurationException if an error occurs
198: */
199: public void load(String fileName) throws ConfigurationException {
200: try {
201: URL url = ConfigurationUtils.locate(basePath, fileName);
202:
203: if (url == null) {
204: throw new ConfigurationException(
205: "Cannot locate configuration source "
206: + fileName);
207: }
208: load(url);
209: } catch (ConfigurationException e) {
210: throw e;
211: } catch (Exception e) {
212: throw new ConfigurationException(e.getMessage(), e);
213: }
214: }
215:
216: /**
217: * Load the configuration from the specified file. This does not change
218: * the source of the configuration (i.e. the internally maintained file
219: * name). Use one of the setter methods for this purpose.
220: *
221: * @param file the file to load
222: * @throws ConfigurationException if an error occurs
223: */
224: public void load(File file) throws ConfigurationException {
225: try {
226: load(file.toURL());
227: } catch (ConfigurationException e) {
228: throw e;
229: } catch (Exception e) {
230: throw new ConfigurationException(e.getMessage(), e);
231: }
232: }
233:
234: /**
235: * Load the configuration from the specified URL. This does not change the
236: * source of the configuration (i.e. the internally maintained file name).
237: * Use on of the setter methods for this purpose.
238: *
239: * @param url the URL of the file to be loaded
240: * @throws ConfigurationException if an error occurs
241: */
242: public void load(URL url) throws ConfigurationException {
243: if (sourceURL == null) {
244: if (StringUtils.isEmpty(getBasePath())) {
245: // ensure that we have a valid base path
246: setBasePath(url.toString());
247: }
248: sourceURL = url;
249: }
250:
251: // throw an exception if the target URL is a directory
252: File file = ConfigurationUtils.fileFromURL(url);
253: if (file != null && file.isDirectory()) {
254: throw new ConfigurationException(
255: "Cannot load a configuration from a directory");
256: }
257:
258: InputStream in = null;
259:
260: try {
261: in = url.openStream();
262: load(in);
263: } catch (ConfigurationException e) {
264: throw e;
265: } catch (Exception e) {
266: throw new ConfigurationException(e.getMessage(), e);
267: } finally {
268: // close the input stream
269: try {
270: if (in != null) {
271: in.close();
272: }
273: } catch (IOException e) {
274: getLogger().warn("Could not close input stream", e);
275: }
276: }
277: }
278:
279: /**
280: * Load the configuration from the specified stream, using the encoding
281: * returned by {@link #getEncoding()}.
282: *
283: * @param in the input stream
284: *
285: * @throws ConfigurationException if an error occurs during the load operation
286: */
287: public void load(InputStream in) throws ConfigurationException {
288: load(in, getEncoding());
289: }
290:
291: /**
292: * Load the configuration from the specified stream, using the specified
293: * encoding. If the encoding is null the default encoding is used.
294: *
295: * @param in the input stream
296: * @param encoding the encoding used. <code>null</code> to use the default encoding
297: *
298: * @throws ConfigurationException if an error occurs during the load operation
299: */
300: public void load(InputStream in, String encoding)
301: throws ConfigurationException {
302: Reader reader = null;
303:
304: if (encoding != null) {
305: try {
306: reader = new InputStreamReader(in, encoding);
307: } catch (UnsupportedEncodingException e) {
308: throw new ConfigurationException(
309: "The requested encoding is not supported, try the default encoding.",
310: e);
311: }
312: }
313:
314: if (reader == null) {
315: reader = new InputStreamReader(in);
316: }
317:
318: load(reader);
319: }
320:
321: /**
322: * Save the configuration. Before this method can be called a valid file
323: * name must have been set.
324: *
325: * @throws ConfigurationException if an error occurs or no file name has
326: * been set yet
327: */
328: public void save() throws ConfigurationException {
329: if (getFileName() == null) {
330: throw new ConfigurationException(
331: "No file name has been set!");
332: }
333:
334: if (sourceURL != null) {
335: save(sourceURL);
336: } else {
337: save(fileName);
338: }
339: strategy.init();
340: }
341:
342: /**
343: * Save the configuration to the specified file. This doesn't change the
344: * source of the configuration, use setFileName() if you need it.
345: *
346: * @param fileName the file name
347: *
348: * @throws ConfigurationException if an error occurs during the save operation
349: */
350: public void save(String fileName) throws ConfigurationException {
351: try {
352: File file = ConfigurationUtils.getFile(basePath, fileName);
353: if (file == null) {
354: throw new ConfigurationException(
355: "Invalid file name for save: " + fileName);
356: }
357: save(file);
358: } catch (ConfigurationException e) {
359: throw e;
360: } catch (Exception e) {
361: throw new ConfigurationException(e.getMessage(), e);
362: }
363: }
364:
365: /**
366: * Save the configuration to the specified URL if it's a file URL.
367: * This doesn't change the source of the configuration, use setURL()
368: * if you need it.
369: *
370: * @param url the URL
371: *
372: * @throws ConfigurationException if an error occurs during the save operation
373: */
374: public void save(URL url) throws ConfigurationException {
375: File file = ConfigurationUtils.fileFromURL(url);
376: if (file != null) {
377: save(file);
378: } else {
379: throw new ConfigurationException("Could not save to URL "
380: + url);
381: }
382: }
383:
384: /**
385: * Save the configuration to the specified file. The file is created
386: * automatically if it doesn't exist. This doesn't change the source
387: * of the configuration, use {@link #setFile} if you need it.
388: *
389: * @param file the target file
390: *
391: * @throws ConfigurationException if an error occurs during the save operation
392: */
393: public void save(File file) throws ConfigurationException {
394: OutputStream out = null;
395:
396: try {
397: // create the file if necessary
398: createPath(file);
399: out = new FileOutputStream(file);
400: save(out);
401: } catch (IOException e) {
402: throw new ConfigurationException(e.getMessage(), e);
403: } finally {
404: // close the output stream
405: try {
406: if (out != null) {
407: out.close();
408: }
409: } catch (IOException e) {
410: getLogger().warn("Could not close output stream", e);
411: }
412: }
413: }
414:
415: /**
416: * Save the configuration to the specified stream, using the encoding
417: * returned by {@link #getEncoding()}.
418: *
419: * @param out the output stream
420: *
421: * @throws ConfigurationException if an error occurs during the save operation
422: */
423: public void save(OutputStream out) throws ConfigurationException {
424: save(out, getEncoding());
425: }
426:
427: /**
428: * Save the configuration to the specified stream, using the specified
429: * encoding. If the encoding is null the default encoding is used.
430: *
431: * @param out the output stream
432: * @param encoding the encoding to use
433: * @throws ConfigurationException if an error occurs during the save operation
434: */
435: public void save(OutputStream out, String encoding)
436: throws ConfigurationException {
437: Writer writer = null;
438:
439: if (encoding != null) {
440: try {
441: writer = new OutputStreamWriter(out, encoding);
442: } catch (UnsupportedEncodingException e) {
443: throw new ConfigurationException(
444: "The requested encoding is not supported, try the default encoding.",
445: e);
446: }
447: }
448:
449: if (writer == null) {
450: writer = new OutputStreamWriter(out);
451: }
452:
453: save(writer);
454: }
455:
456: /**
457: * Return the name of the file.
458: *
459: * @return the file name
460: */
461: public String getFileName() {
462: return fileName;
463: }
464:
465: /**
466: * Set the name of the file. The passed in file name can contain a
467: * relative path.
468: * It must be used when referring files with relative paths from classpath.
469: * Use <code>{@link AbstractFileConfiguration#setPath(String)
470: * setPath()}</code> to set a full qualified file name.
471: *
472: * @param fileName the name of the file
473: */
474: public void setFileName(String fileName) {
475: sourceURL = null;
476: this .fileName = fileName;
477: }
478:
479: /**
480: * Return the base path.
481: *
482: * @return the base path
483: * @see FileConfiguration#getBasePath()
484: */
485: public String getBasePath() {
486: return basePath;
487: }
488:
489: /**
490: * Sets the base path. The base path is typically either a path to a
491: * directory or a URL. Together with the value passed to the
492: * <code>setFileName()</code> method it defines the location of the
493: * configuration file to be loaded. The strategies for locating the file are
494: * quite tolerant. For instance if the file name is already an absolute path
495: * or a fully defined URL, the base path will be ignored. The base path can
496: * also be a URL, in which case the file name is interpreted in this URL's
497: * context. Because the base path is used by some of the derived classes for
498: * resolving relative file names it should contain a meaningful value. If
499: * other methods are used for determining the location of the configuration
500: * file (e.g. <code>setFile()</code> or <code>setURL()</code>), the
501: * base path is automatically set.
502: *
503: * @param basePath the base path.
504: */
505: public void setBasePath(String basePath) {
506: sourceURL = null;
507: this .basePath = basePath;
508: }
509:
510: /**
511: * Return the file where the configuration is stored. If the base path is a
512: * URL with a protocol different than "file", or the configuration
513: * file is within a compressed archive, the return value
514: * will not point to a valid file object.
515: *
516: * @return the file where the configuration is stored; this can be <b>null</b>
517: */
518: public File getFile() {
519: if (getFileName() == null) {
520: return null;
521: } else {
522: if (sourceURL != null) {
523: return ConfigurationUtils.fileFromURL(sourceURL);
524: } else {
525: return ConfigurationUtils.getFile(getBasePath(),
526: getFileName());
527: }
528: }
529: }
530:
531: /**
532: * Set the file where the configuration is stored. The passed in file is
533: * made absolute if it is not yet. Then the file's path component becomes
534: * the base path and its name component becomes the file name.
535: *
536: * @param file the file where the configuration is stored
537: */
538: public void setFile(File file) {
539: sourceURL = null;
540: setFileName(file.getName());
541: setBasePath((file.getParentFile() != null) ? file
542: .getParentFile().getAbsolutePath() : null);
543: }
544:
545: /**
546: * Returns the full path to the file this configuration is based on. The
547: * return value is a valid File path only if this configuration is based on
548: * a file on the local disk.
549: * If the configuration was loaded from a packed archive the returned value
550: * is the string form of the URL from which the configuration was loaded.
551: *
552: * @return the full path to the configuration file
553: */
554: public String getPath() {
555: String path = null;
556: File file = getFile();
557: // if resource was loaded from jar file may be null
558: if (file != null) {
559: path = file.getAbsolutePath();
560: }
561:
562: // try to see if file was loaded from a jar
563: if (path == null) {
564: if (sourceURL != null) {
565: path = sourceURL.getPath();
566: } else {
567: try {
568: path = ConfigurationUtils.getURL(getBasePath(),
569: getFileName()).getPath();
570: } catch (MalformedURLException e) {
571: // simply ignore it and return null
572: ;
573: }
574: }
575: }
576:
577: return path;
578: }
579:
580: /**
581: * Sets the location of this configuration as a full or relative path name.
582: * The passed in path should represent a valid file name on the file system.
583: * It must not be used to specify relative paths for files that exist
584: * in classpath, either plain file system or compressed archive,
585: * because this method expands any relative path to an absolute one which
586: * may end in an invalid absolute path for classpath references.
587: *
588: * @param path the full path name of the configuration file
589: */
590: public void setPath(String path) {
591: setFile(new File(path));
592: }
593:
594: /**
595: * Return the URL where the configuration is stored.
596: *
597: * @return the configuration's location as URL
598: */
599: public URL getURL() {
600: return (sourceURL != null) ? sourceURL : ConfigurationUtils
601: .locate(getBasePath(), getFileName());
602: }
603:
604: /**
605: * Set the location of this configuration as a URL. For loading this can be
606: * an arbitrary URL with a supported protocol. If the configuration is to
607: * be saved, too, a URL with the "file" protocol should be
608: * provided.
609: *
610: * @param url the location of this configuration as URL
611: */
612: public void setURL(URL url) {
613: setBasePath(ConfigurationUtils.getBasePath(url));
614: setFileName(ConfigurationUtils.getFileName(url));
615: sourceURL = url;
616: }
617:
618: public void setAutoSave(boolean autoSave) {
619: this .autoSave = autoSave;
620: }
621:
622: public boolean isAutoSave() {
623: return autoSave;
624: }
625:
626: /**
627: * Save the configuration if the automatic persistence is enabled
628: * and if a file is specified.
629: */
630: protected void possiblySave() {
631: if (autoSave && fileName != null) {
632: try {
633: save();
634: } catch (ConfigurationException e) {
635: throw new ConfigurationRuntimeException(
636: "Failed to auto-save", e);
637: }
638: }
639: }
640:
641: /**
642: * Adds a new property to this configuration. This implementation checks if
643: * the auto save mode is enabled and saves the configuration if necessary.
644: *
645: * @param key the key of the new property
646: * @param value the value
647: */
648: public void addProperty(String key, Object value) {
649: super .addProperty(key, value);
650: possiblySave();
651: }
652:
653: /**
654: * Sets a new value for the specified property. This implementation checks
655: * if the auto save mode is enabled and saves the configuration if
656: * necessary.
657: *
658: * @param key the key of the affected property
659: * @param value the value
660: */
661: public void setProperty(String key, Object value) {
662: super .setProperty(key, value);
663: possiblySave();
664: }
665:
666: public void clearProperty(String key) {
667: super .clearProperty(key);
668: possiblySave();
669: }
670:
671: public ReloadingStrategy getReloadingStrategy() {
672: return strategy;
673: }
674:
675: public void setReloadingStrategy(ReloadingStrategy strategy) {
676: this .strategy = strategy;
677: strategy.setConfiguration(this );
678: strategy.init();
679: }
680:
681: /**
682: * Performs a reload operation if necessary. This method is called on each
683: * access of this configuration. It asks the associated reloading strategy
684: * whether a reload should be performed. If this is the case, the
685: * configuration is cleared and loaded again from its source. If this
686: * operation causes an exception, the registered error listeners will be
687: * notified. The error event passed to the listeners is of type
688: * <code>EVENT_RELOAD</code> and contains the exception that caused the
689: * event.
690: */
691: public void reload() {
692: synchronized (reloadLock) {
693: if (noReload == 0) {
694: try {
695: enterNoReload(); // avoid reentrant calls
696:
697: if (strategy.reloadingRequired()) {
698: if (getLogger().isInfoEnabled()) {
699: getLogger().info(
700: "Reloading configuration. URL is "
701: + getURL());
702: }
703: fireEvent(EVENT_RELOAD, null, getURL(), true);
704: setDetailEvents(false);
705: try {
706: clear();
707: load();
708: } finally {
709: setDetailEvents(true);
710: }
711: fireEvent(EVENT_RELOAD, null, getURL(), false);
712:
713: // notify the strategy
714: strategy.reloadingPerformed();
715: }
716: } catch (Exception e) {
717: fireError(EVENT_RELOAD, null, null, e);
718: // todo rollback the changes if the file can't be reloaded
719: } finally {
720: exitNoReload();
721: }
722: }
723: }
724: }
725:
726: /**
727: * Enters the "No reloading mode". As long as this mode is active
728: * no reloading will be performed. This is necessary for some
729: * implementations of <code>save()</code> in derived classes, which may
730: * cause a reload while accessing the properties to save. This may cause the
731: * whole configuration to be erased. To avoid this, this method can be
732: * called first. After a call to this method there always must be a
733: * corresponding call of <code>{@link #exitNoReload()}</code> later! (If
734: * necessary, <code>finally</code> blocks must be used to ensure this.
735: */
736: protected void enterNoReload() {
737: synchronized (reloadLock) {
738: noReload++;
739: }
740: }
741:
742: /**
743: * Leaves the "No reloading mode".
744: *
745: * @see #enterNoReload()
746: */
747: protected void exitNoReload() {
748: synchronized (reloadLock) {
749: if (noReload > 0) // paranoia check
750: {
751: noReload--;
752: }
753: }
754: }
755:
756: /**
757: * Sends an event to all registered listeners. This implementation ensures
758: * that no reloads are performed while the listeners are invoked. So
759: * infinite loops can be avoided that can be caused by event listeners
760: * accessing the configuration's properties when they are invoked.
761: *
762: * @param type the event type
763: * @param propName the name of the property
764: * @param propValue the value of the property
765: * @param before the before update flag
766: */
767: protected void fireEvent(int type, String propName,
768: Object propValue, boolean before) {
769: enterNoReload();
770: try {
771: super .fireEvent(type, propName, propValue, before);
772: } finally {
773: exitNoReload();
774: }
775: }
776:
777: public Object getProperty(String key) {
778: reload();
779: return super .getProperty(key);
780: }
781:
782: public boolean isEmpty() {
783: reload();
784: return super .isEmpty();
785: }
786:
787: public boolean containsKey(String key) {
788: reload();
789: return super .containsKey(key);
790: }
791:
792: public Iterator getKeys() {
793: reload();
794: return super .getKeys();
795: }
796:
797: /**
798: * Create the path to the specified file.
799: *
800: * @param file the target file
801: */
802: private void createPath(File file) {
803: if (file != null) {
804: // create the path to the file if the file doesn't exist
805: if (!file.exists()) {
806: File parent = file.getParentFile();
807: if (parent != null && !parent.exists()) {
808: parent.mkdirs();
809: }
810: }
811: }
812: }
813:
814: public String getEncoding() {
815: return encoding;
816: }
817:
818: public void setEncoding(String encoding) {
819: this .encoding = encoding;
820: }
821:
822: /**
823: * Creates a copy of this configuration. The new configuration object will
824: * contain the same properties as the original, but it will lose any
825: * connection to a source file (if one exists); this includes setting the
826: * source URL, base path, and file name to <b>null</b>. This is done to
827: * avoid race conditions if both the original and the copy are modified and
828: * then saved.
829: *
830: * @return the copy
831: * @since 1.3
832: */
833: public Object clone() {
834: AbstractFileConfiguration copy = (AbstractFileConfiguration) super
835: .clone();
836: copy.setBasePath(null);
837: copy.setFileName(null);
838: copy.initReloadingStrategy();
839: return copy;
840: }
841:
842: /**
843: * Helper method for initializing the reloading strategy.
844: */
845: private void initReloadingStrategy() {
846: setReloadingStrategy(new InvariantReloadingStrategy());
847: }
848: }
|