View Javadoc

1   /*
2    * Copyright (C) 2004 Christian Siefkes <christian@siefkes.net>.
3    * Development of this software is supported by the German Research Society,
4    * Berlin-Brandenburg Graduate School in Distributed Information Systems
5    * (DFG grant no. GRK 316).
6    *
7    * This library is free software; you can redistribute it and/or
8    * modify it under the terms of the GNU Lesser General Public
9    * License as published by the Free Software Foundation; either
10   * version 2.1 of the License, or (at your option) any later version.
11   *
12   * This library is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   * Lesser General Public License for more details.
16   *
17   * You should have received a copy of the GNU Lesser General Public
18   * License along with this library; if not, visit
19   * http://www.gnu.org/licenses/lgpl.html or write to the Free Software
20   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
21   */
22  package de.fu_berlin.ties;
23  
24  import java.io.File;
25  import java.io.InputStream;
26  import java.util.Collections;
27  import java.util.Iterator;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.SortedSet;
32  import java.util.TreeSet;
33  
34  import org.apache.commons.configuration.CompositeConfiguration;
35  import org.apache.commons.configuration.Configuration;
36  import org.apache.commons.configuration.ConfigurationException;
37  import org.apache.commons.configuration.ConfigurationKey;
38  import org.apache.commons.configuration.PropertiesConfiguration;
39  import org.apache.commons.configuration.XMLConfiguration;
40  import org.apache.commons.lang.StringUtils;
41  import org.apache.commons.lang.builder.ToStringBuilder;
42  
43  import de.fu_berlin.ties.io.IOUtils;
44  import de.fu_berlin.ties.util.Util;
45  
46  /***
47   * A composite configuration that allows to localize of keys and to querying
48   * types and descriptions of entries.
49   *
50   * @author Christian Siefkes
51   * @version $Revision: 1.24 $, $Date: 2004/10/30 17:25:28 $, $Author: siefkes $
52   */
53  public class TiesConfiguration extends CompositeConfiguration {
54  
55      /***
56       * The main configuration object for TIES, loaded via
57       * {@link #TiesConfiguration(String)} using "ties" as base name.
58       * <strong>You should NOT modify the configuration properties stored in this
59       * object, especially since it is not fully thread-safe (unless you know
60       * what you're doing and are sure there are no other objects around that
61       * might try to access the configuration, e.g. at the begin of a
62       * program's <code>main</code> method).</strong>
63       */
64      public static final TiesConfiguration CONF = new TiesConfiguration("ties");
65  
66      /***
67       * The subdirectory in the class path containing config files.
68       */
69      public static final String CONF_DIR = "conf";
70  
71      /***
72       * The extension of config files in {@link PropertiesConfiguration}
73       * format.
74       */
75      public static final String CONF_EXTENSION = ".cfg";
76  
77      /***
78       * The extension of config files in {@linkplain XMLConfiguration XML
79       * format}.
80       */
81      public static final String XML_EXTENSION = ".xml";
82  
83      /***
84       * The extension of
85       * {@linkplain #addDescriptorConfig(Configuration) descriptor configuration}
86       * files (in {@link PropertiesConfiguration} format).
87       */
88      public static final String DESC_EXTENSION = ".desc";
89  
90      /***
91       * Configuration key prefix mapping goals to fully qualified class names.
92       */
93      public static final String CONFIG_GOAL_PREFIX = "goal";
94  
95      /***
96       * Special configuration key: the language of documents, using the <a
97       * href="http://www.loc.gov/standards/iso639-2/englangn.html">ISO 639</a>
98       * language codes (2-letter codes where available, e.g. "en" (English) or
99       * "de" (German).
100      */
101     public static final String CONFIG_LANG = "lang";
102 
103     /***
104      * The character separating tokens in keys.
105      */
106     private static final char PROPERTY_DELIM = '.';
107 
108 
109     /***
110      * Creates a adapted (caller-specific) key by joining a base name
111      * with a suffix. If the adapted form of the key doesn't exist in this
112      * configuration, the base form is returned instead.
113      *
114      * @param baseKey the basic key
115      * @param suffix the suffix of the key; if <code>null</code> the
116      * <code>baseKey</code> is returned
117      * @return the full key, adapted by joining with the {@link #CONFIG_LANG}
118      * key; resp. the base form if the full key doesn't exist or
119      * <code>suffix</code> is <code>null</code>
120      * @see #joinKey(String, String)
121      */
122     public String adaptKey(final String baseKey, final String suffix) {
123         if (suffix != null) {
124             final String adaptedKey = joinKey(baseKey, suffix);
125 
126             // return adapted key only if it exists, base form otherwise
127             if (containsKey(adaptedKey)) {
128                 return adaptedKey;
129             } else {
130                 return baseKey;
131             }
132         } else {
133             return baseKey;
134         }
135     }
136 
137     /***
138      * Utility method that thorougly checks whether a string array is empty.
139      * It returns <code>true</code> in three cases:
140      *
141      * <ol>
142      * <li>the specified array is <code>null</code>;
143      * <li>the specified array is empty (0 elements);
144      * <li>the specified array contains a single element which is
145      * <code>null</code> or an empty or blank (only whitespace) string.
146      * </ol>
147      * 
148      * This method is useful because some reconfigurations can cause a
149      * <code>TiesConfiguration</code> to return a single-element list containing
150      * only an empty string. Usually such a list should be treated as empty.
151      *
152      * @param array the array to check
153      * @return <code>true</code> iff the list is empty as described above
154      */
155     public static boolean arrayIsEmpty(final String[] array) {
156         final boolean result;
157 
158         if ((array == null) || array.length == 0) {
159             // null or empty array
160             result = true;
161         } else if (array.length == 1) {
162             // only one element: check if it's null, empty, or whitespace
163             result = StringUtils.isBlank(array[0]);
164         } else {
165             result = false;
166         }
167 
168         return result;
169     }
170 
171     /***
172      * Utility method that thorougly checks whether a list is empty. It returns
173      * <code>true</code> in three cases:
174      *
175      * <ol>
176      * <li>the specified list is <code>null</code>;
177      * <li>the specified list is empty (0 elements);
178      * <li>the specified list contains a single element which is
179      * <code>null</code> or an empty or blank (only whitespace) string.
180      * </ol>
181      * 
182      * This method is useful because some reconfigurations can cause a
183      * <code>TiesConfiguration</code> to return a single-element list containing
184      * only an empty string. Usually such a list should be treated as empty.
185      *
186      * @param list the list to check
187      * @return <code>true</code> iff the list is empty as described above
188      */
189     public static boolean listIsEmpty(final List list) {
190         final boolean result;
191 
192         if ((list == null) || list.isEmpty()) {
193             // null or empty list
194             result = true;
195         } else if (list.size() == 1) {
196             // only one element: check if it's the empty string (or null)
197             final Object first = list.get(0);
198 
199             if (first == null) {
200                 // single element is null
201                 result = true;
202             } else if (first instanceof String) {
203                 // true iff single element is an empty or blank string
204                 result = StringUtils.isBlank((String) first);
205             } else {
206                 result = false;
207             }
208         } else {
209             result = false;
210         }
211 
212         return result;
213     }
214 
215     /***
216      * Utility method that thorougly checks whether a property (as returned
217      * by {@link Configuration#getProperty(String)} is empty. It returns
218      * <code>true</code> in two cases:
219      *
220      * <ol>
221      * <li>the specified object is <code>null</code>;
222      * <li>the specified object is an empty or blank (only whitespace) string.
223      * </ol>
224      * 
225      * This method is useful because some reconfigurations can cause a
226      * <code>TiesConfiguration</code> to return a empty strings.
227      *
228      * @param object the object to check
229      * @return <code>true</code> iff the object is empty as described above
230      */
231     public static boolean propertyIsEmpty(final Object object) {
232         final boolean result;
233 
234         if (object == null) {
235             // null object
236             result = true;
237         } else if (object instanceof String) {
238             // test if string is empty
239             result = StringUtils.isBlank((String) object);
240         } else {
241             result = false;
242         }
243 
244         return result;
245     }
246 
247     /***
248      * Creates a full key by joining a prefix and a suffix string, separated by
249      * the property delimiter.
250      *
251      * @param prefix the prefix of the key
252      * @param suffix the suffix of the key
253      * @return the full key (prefix + delimiter + suffix)
254      */
255     public static String joinKey(final String prefix, final String suffix) {
256         return new ConfigurationKey(prefix).append(suffix).toString();
257     }
258 
259 
260      /***
261      * An inner class wrapping descriptor information on an entry: type
262      * of the entry, whether it is optional or a list, a description of the
263      * entry.
264      */
265     public final class EntryDescriptor {
266 
267         /***
268          * The key (name) of this entry.
269          */
270         private final String key;
271 
272         /***
273          * The base type of the entry (String, Integer etc.).
274          */
275         private final String type;
276 
277         /***
278          * Whether the entry is optional.
279          */
280         private final boolean optional;
281 
282         /***
283          * Whether the entry is a list (can contain multiple values).
284          */
285         private final boolean list;
286 
287         /***
288          * A textual description of the entry.
289          */
290         private final String description;
291 
292         /***
293          * Returns a textual description of the entry.
294          * @return the value of the attribute
295          */
296         public String getDescription() {
297             return description;
298         }
299 
300         /***
301          * Creates a new instance.
302          *
303          * @param keyName the key (name) of this entry
304          * @param typ the base type of the entry
305          * @param isOpt whether the entry is optional
306          * @param isList whether the entry is a list
307          * @param desc a textual description of the entry
308          */
309         private EntryDescriptor(final String keyName, final String typ,
310                 final boolean isOpt, final boolean isList, final String desc) {
311             key = keyName;
312             type = typ;
313             optional = isOpt;
314             list = isList;
315             description = desc;
316         }
317 
318         /***
319          * Reads the value of this property as a string list and returns the
320          * element at the specified position.
321          *
322          * @param index the index of element to return
323          * @return the element at the specified position in this list; or
324          * <code>null</code> if the position does not exist in the list or
325          * no value exists for this key
326          * @throws IndexOutOfBoundsException if <code>index</code> &lt; 0
327          */
328         public String getElement(final int index)
329                 throws IndexOutOfBoundsException {
330             // ensure value is present
331             if (containsKey(key)) {
332                 final List value = getList(key);
333 
334                 // ensure element is present
335                 if (index < value.size()) {
336                     return (String) value.get(index);
337                 } else {
338                     return null;
339                 }
340             } else {
341                 return null;
342             }
343         }
344 
345         /***
346          * Reads the value of this property as a string list and returns a
347          * sublist starting at the specified position.
348          *
349          * @param fromIndex the index of the first element to return (inclusive)
350          * @return a sublist starting at the specified position in this list; or
351          * <code>null</code> if the position does not exist in the list or
352          * no value exists for this key
353          * @throws IndexOutOfBoundsException if <code>fromIndex</code> &lt; 0
354          */
355         public List getElementsFrom(final int fromIndex)
356                 throws IndexOutOfBoundsException {
357             // ensure value is present
358             if (containsKey(key)) {
359                 final List value = getList(key);
360 
361                 // ensure there is at least one element to return
362                 if (fromIndex < value.size()) {
363                     return value.subList(fromIndex, value.size());
364                 } else {
365                     return null;
366                 }
367             } else {
368                 return null;
369             }
370         }
371 
372         /***
373          * Returns the key (name) of this entry.
374          * @return the value of the attribute
375          */
376         public String getKey() {
377             return key;
378         }
379 
380         /***
381          * Returns the base type of the entry (String, Integer etc.).
382          * @return the value of the attribute
383          */
384         public String getType() {
385             return type;
386         }
387 
388         /***
389          * Returns the value of this property (as an object).
390          * @return the value of this property; or <code>null</code> if no
391          * value is set
392          */
393         public Object getValue() {
394             // delegate to outer class if key is present
395             return containsKey(key) ? getProperty(key) : null;
396         }
397 
398         /***
399          * Whether the entry is a list (can contain multiple values).
400          * @return the value of the attribute
401          */
402         public boolean isList() {
403             return list;
404         }
405 
406         /***
407          * Whether the entry is optional.
408          * @return the value of the attribute
409          */
410         public boolean isOptional() {
411             return optional;
412         }
413 
414         /***
415          * Returns a string representation of this object.
416          *
417          * @return a textual representation
418          */
419         public String toString() {
420             return new ToStringBuilder(this)
421                 .append("key", key)
422                 .append("type", type)
423                 .append("optional", optional)
424                 .append("list", list)
425                 .append("description", description)
426                 .toString();
427         }
428 
429     }
430 
431 
432     /***
433      * Describes the entries in this config. Each value must be a two-array,
434      * specifying the type of the entry and a description of the entry.
435      */
436     private final CompositeConfiguration descriptorConfig =
437         new CompositeConfiguration();
438 
439 
440     /***
441      * Creates a new empty instance.
442      */
443     public TiesConfiguration() {
444         super();
445     }
446 
447     /***
448      * Creates a new instance, delegating to
449      * {@link #addConfiguration(Configuration, Configuration)}.
450      *
451      * @param config the initial configuration to wrap
452      * @param desc the initial descriptor config to wrap (cf.
453      * {@link #addDescriptorConfig(Configuration)} for the required contents)
454      */
455     public TiesConfiguration(final Configuration config,
456                              final Configuration desc) {
457         this();
458         addConfiguration(config, desc);
459     }
460 
461     /***
462      * Creates a new instance, delegating to {@link #load(String)}.
463      *
464      * @param baseName the base name of the configuration
465      */
466     public TiesConfiguration(final String baseName) {
467         this();
468         load(baseName);
469     }
470 
471     /***
472      * Adds a configuration and a corresponding descriptor config.
473      *
474      * @param config the configuration to add
475      * @param desc the descriptor config to add (cf.
476      * {@link #addDescriptorConfig(Configuration)} for the required contents)
477      */
478     public void addConfiguration(final Configuration config,
479                                  final Configuration desc) {
480         addConfiguration(config);
481         addDescriptorConfig(desc);
482     }
483 
484     /***
485      * Adds a descriptor configuration that can be consulted to query the
486      * type and use of a entries. Each value must be a two-array specifying
487      * the type (first element) and a description (second element) of the key
488      * as used in the main configuration. Append '?' to the type if it is
489      * optional (zero or one values), '*' if it is an optional list (zero or
490      * more values), or '+' if it is a required list (one or more values).
491      *
492      * @param desc the descriptor configuration to add
493      */
494     public void addDescriptorConfig(final Configuration desc) {
495         descriptorConfig.addConfiguration(desc);
496     }
497 
498     /***
499      * Modifies configuration properties from <code>[+|-]key[=value]</code>
500      * pairs in a string array. Delegates to
501      * {@link #modifyProperty(String, boolean)} for each array element
502      * starting with "-" (using overwrite mode) or "+" (using append mode).
503      * Other array elements are collected and returned.
504      *
505      * @param args the array of strings to parse
506      * @return a list of all arguments that do not start with "+" or "-"
507      * @throws IllegalArgumentException if one of the
508      * <code>[+|-]key[=value]</code> pairs doesn't contain a key ("=" is second
509      * character)
510      */
511     public List configureFromArgs(final String[] args)
512             throws IllegalArgumentException {
513         final List<String> otherArgs = new LinkedList<String>();
514         for (int i = 0; i < args.length; i++) {
515             if (args[i].startsWith("-")) {
516                 modifyProperty(args[i].substring(1), true);
517             } else if (args[i].startsWith("+")) {
518                 modifyProperty(args[i].substring(1), false);
519             } else {
520                 // adds to other arguments
521                 otherArgs.add(args[i]);
522             }
523         }
524         return otherArgs;
525     }
526 
527     /***
528      * Returns the descriptor for a given key, if any is given in the
529      * {@linkplain #addDescriptorConfig(Configuration) descriptor
530      * configuration}.
531      *
532      * @param key the key to describe
533      * @return a descriptor object for this key, or <code>null</code> is none
534      * is given
535      */
536     public EntryDescriptor getDescriptor(final String key) {
537         final EntryDescriptor result;
538         final String[] descEntry;
539         final String suffixDesc;
540 
541         if (descriptorConfig.containsKey(key)) {
542             descEntry = descriptorConfig.getStringArray(key);
543             suffixDesc = null;
544         } else {
545             // split last part from key (language or format-specific) + add
546             // specific description in paranthesis
547             final int lastDelim = key.lastIndexOf(PROPERTY_DELIM);
548 
549             if (lastDelim > 0) {
550                 final String mainKey = key.substring(0, lastDelim);
551                 final String suffix = key.substring(lastDelim);
552                 descEntry = descriptorConfig.getStringArray(mainKey);
553                 suffixDesc = descriptorConfig.getString(suffix, null);
554             } else {
555                 descEntry = null;
556                 suffixDesc = null;
557             }
558         }
559 
560         if (descEntry != null) {
561             if (descEntry.length >= 2) {
562                 // first element contains type info
563                 final String firstElem = descEntry[0].trim();
564                 final String type;
565                 final boolean optional;
566                 final boolean list;
567 
568                 // check for (and remove) special chars ?*+ at end of string
569                 if (firstElem.endsWith("?")) {
570                     optional = true;
571                     list = false;
572                     type = StringUtils.chop(firstElem);
573                 } else if (firstElem.endsWith("*")) {
574                     optional = true;
575                     list = true;
576                     type = StringUtils.chop(firstElem);
577                 } else if (firstElem.endsWith("+")) {
578                     optional = false;
579                     list = true;
580                     type = StringUtils.chop(firstElem);
581                 } else {
582                     optional = false;
583                     list = false;
584                     type = firstElem;
585                 }
586 
587                 // second element contains actual description
588                 String description = descEntry[1];
589 
590                 // append suffix description if paranthesis, if given
591                 if (!StringUtils.isEmpty(suffixDesc)) {
592                     description += " (" + suffixDesc + ")";
593                 }
594 
595                 result =
596                     new EntryDescriptor(key, type, optional, list, description);
597             } else {
598                 result = null;
599             }
600 
601             // log warning if there are too few or to many elements
602             if ((descEntry.length == 1) || (descEntry.length > 2)) {
603                 Util.LOG.error("Descriptor entry for " + key
604                     + " has " + descEntry.length + " elements instead of 2");
605             }
606         } else {
607             result = null;
608         }
609 
610         return result;
611     }
612 
613     /***
614      * Read a property from this configuration. If the given key does not exist
615      * in this configuration, it is looked up as a
616      * {@linkplain System#getProperty(java.lang.String) Java system property}.
617      *
618      * @param key key to use for mapping
619      * @return object associated with the given configuration key
620      */
621     protected Object getPropertyDirect(final String key) {
622         Object result = super.getPropertyDirect(key);
623 
624         // if the superclass doesn't know it, maybe it's a Java system property?
625         if (result == null) {
626             try {
627                 result = System.getProperty(key);
628             } catch (SecurityException se) {
629                 // not allowed to look it up
630                 result = null;
631             }
632         }
633 
634         return result;
635     }
636 
637     /***
638      * Get an array of strings associated with the given configuration key.
639      *
640      * @param key The configuration key
641      * @return The associated string array if key is found.
642      */
643     public String[] getStringArray(final String key) {
644         final String[] result = super.getStringArray(key);
645 
646         if ((result.length == 1) && (StringUtils.isBlank(result[0]))) {
647             // convert array containg a single null/empty/blank element into
648             // an empty array
649             return new String[0];
650         } else {
651             return result;
652         }
653     }
654 
655     /***
656      * Copies all properties contained in this instance to a given
657      * configuration. This method can be used for storing this configuration in
658      * any format desired, by creating an empty configuration of the
659      * requested type and passing it to this method. Keys are sorted in
660      * alphabetic order.
661      *
662      * @param appender the configuration to append all properties to
663      */
664     public void flatten(final Configuration appender) {
665         final Iterator keys = sortedKeys().iterator();
666         String key;
667         Object value;
668 
669         while (keys.hasNext()) {
670             key = (String) keys.next();
671             value = getProperty(key);
672             appender.addProperty(key, value);
673         }
674     }
675 
676     /***
677      * Loads configuration in {@link PropertiesConfiguration} or
678      * {@linkplain org.apache.commons.configuration.XMLConfiguration XML}
679      * format. Combines configuration properties from several resources if
680      * they exist:
681      *
682      * <ol>
683      * <li>The files <code><em>baseName</em>.xml</code> resp.
684      * <code><em>baseName</em>.cfg</code> in the current working
685      * directory, if they exist and are readable files</li>
686      * <li>The file <code><em>baseName</em>.xml</code> resp.
687      * <code><em>baseName</em>.cfg</code> in the user's home
688      * directory, if they exist and are readable files</li>
689      * <li>The file <code><em>baseName</em>.cfg</code> in the <code>conf</code>
690      * subdirectory in the classpath (e.g. from a jar file) -- <strong>this
691      * classpath resource should always exist (otherwise an warning will be
692      * logged and later errors are likely)</strong></li>
693      * </ol>
694      *
695      * <p>For shared keys, the first match will be returned. If corresponding
696      * <code><em>baseName</em>.desc</code> exists, they are assumed to
697      * contain {@linkplain #addDescriptorConfig(Configuration) descriptors}.
698      *
699      * <p>*.cfg and *.desc files must must use the
700      * {@link PropertiesConfiguration} format, *.xml files must use the
701      * {@linkplain org.apache.commons.configuration.XMLConfiguration XML}
702      * format.
703      *
704      * <p>The last resource loaded form the classpath must use UTF-8 character
705      * set (otherwise it could not be shared between systems). The other
706      * *.cfg and *.desc (local files) are assumed to use the platform's default
707      * character set. The character set of XML files is determined in the
708      * standard way.
709      *
710      * @param baseName the base name of the configuration
711      */
712     public void load(final String baseName) {
713         // directories to check
714         final String[] dirCandidates = {
715             System.getProperty("user.dir"),
716             System.getProperty("user.home")
717         };
718         File file;
719 
720         // check .xml + .cfg + .desc files in each directory
721         for (int i = 0; i < dirCandidates.length; i++) {
722             // check .xml file (XML format)
723             file = new File(dirCandidates[i] + File.separator + baseName
724                     + XML_EXTENSION);
725             try {
726                 if (file.canRead() && file.isFile()) {
727                     addConfiguration(new XMLConfiguration(file));
728                 }
729 
730                 // check .cfg file (PropertiesConfig format)
731                 file = new File(dirCandidates[i] + File.separator + baseName
732                         + CONF_EXTENSION);
733                 if (file.canRead() && file.isFile()) {
734                     addConfiguration(new PropertiesConfiguration(
735                             file.getAbsolutePath()));
736                 }
737 
738                 // check .desc file (descriptor config in PropertiesConf format)
739                 file = new File(dirCandidates[i] + File.separator + baseName
740                         + DESC_EXTENSION);
741                 if (file.canRead() && file.isFile()) {
742                     addDescriptorConfig(new PropertiesConfiguration(
743                             file.getAbsolutePath()));
744                 }
745             } catch (Exception e) {
746                 Util.LOG.error("Couldn't load configuarion file "
747                     + file.getAbsolutePath() + ": " + e);
748             }
749         }
750 
751         // load default values + descriptors from class path (class path always
752         // uses '/' as separator)
753         String classPathLocation
754             = CONF_DIR + '/' + baseName + CONF_EXTENSION;
755         InputStream in =
756             ClassLoader.getSystemResourceAsStream(classPathLocation);
757 
758         try {
759             if (in != null) {
760                 final PropertiesConfiguration defaults =
761                     new PropertiesConfiguration();
762                 defaults.load(in, IOUtils.STANDARD_UNICODE_CHARSET);
763                 addConfiguration(defaults);
764                 IOUtils.tryToClose(in);
765             } else {
766                 Util.LOG.warn("Didn't find default configuration "
767                     + classPathLocation + " in classpath");
768             }
769 
770             classPathLocation = CONF_DIR + File.separator + baseName
771                 + DESC_EXTENSION;
772             in = ClassLoader.getSystemResourceAsStream(classPathLocation);
773 
774             if (in != null) {
775                     final PropertiesConfiguration defaultDesc =
776                         new PropertiesConfiguration();
777                     defaultDesc.load(in, IOUtils.STANDARD_UNICODE_CHARSET);
778                     addDescriptorConfig(defaultDesc);
779             }
780         } catch (ConfigurationException ce) {
781             Util.LOG.error("Couldn't load default configuration "
782                 + classPathLocation + " from classpath: " + ce);
783         } finally {
784             IOUtils.tryToClose(in);
785         }
786 
787     }
788 
789     /***
790      * Creates a localized (language-specific) key by joining a base name
791      * with the configured language suffix (value mapped to the
792      * {@link #CONFIG_LANG}} key (if this key doesn't exist, the language of
793      * default locale used by the Java Virtual Machine is used).
794      * If the localized form of the key doesn't exist in the specified
795      * configuration, the base form is returned instead. Implemented by
796      * delegating to {@link #adaptKey(String, String)}.
797      *
798      * @param baseKey the basic key
799      * @return the full key, localized by joining with the {@link #CONFIG_LANG}
800      * key
801      */
802     public String localizeKey(final String baseKey) {
803         final String lang = getString(CONFIG_LANG,
804                 Locale.getDefault().getLanguage());
805         return adaptKey(baseKey, lang);
806     }
807 
808     /***
809      * Modifies a configuration property, parsing a <code>key[=value]</code>
810      * pair. Both key and value are trimmed. {@link Boolean#TRUE} is used as
811      * default value if the <code>=value</code> part is omitted.
812      * If "=" is the last character (empty value) and <code>overwrite</code> is
813      * <code>true</code>, the property is removed from the configuration
814      * ({@link Configuration#clearProperty(java.lang.String)}).
815      *
816      * @param keyValue the <code>key=value</code> pair to parse
817      * @param overwrite whether to overwrite (replacing old value: {@link
818      * Configuration#setProperty(java.lang.String, java.lang.Object)}) or
819      * add (append to list, {@link
820      * Configuration#addProperty(java.lang.String, java.lang.Object)}) the
821      * value
822      * @throws IllegalArgumentException if <code>keyValue</code> doesn't contain
823      * a key ("=" is first character)
824      */
825     public void modifyProperty(final String keyValue,
826             final boolean overwrite) throws IllegalArgumentException {
827         // parse string
828         final String key;
829         final Object value;
830         final int equalIndex = keyValue.indexOf('=');
831 
832         if (equalIndex > 0) {
833             key = keyValue.substring(0, equalIndex).trim();
834             value = keyValue.substring(equalIndex + 1).trim();
835         } else if (equalIndex < 0) {
836             // boolean "true" can be omitted (default value)
837             key = keyValue.trim();
838             // specify String instead of boolean to avoid problems during save
839             value = "true";
840         } else {
841             // '=' is first character
842             throw new IllegalArgumentException("Missing key: " + keyValue);
843         }
844 
845         if (overwrite) {
846             // empty string means: delete value
847             if ("".equals(value)) {
848                 clearProperty(key);
849                 Util.LOG.debug("Cleared property " + key);
850             } else {
851                 setProperty(key, value);
852                 Util.LOG.debug("Set property " + key + " = " + value);
853             }
854         } else {
855             addProperty(key, value);
856             Util.LOG.debug("Added to property " + key + ": " + value);
857         }
858     }
859 
860     /***
861      * Saves the contents of this configuration in a file, storing them in
862      * {@link PropertiesConfiguration} format. The platform's default
863      * character set is used.
864      *
865      * @param file the file to use
866      * @throws ConfigurationException if an I/O error occurs during saving
867      */
868     public void save(final File file) throws ConfigurationException {
869         // currently [Base]PropertiesConfig except a String instead of a File
870         save(file.getAbsolutePath());
871     }
872 
873     /***
874      * Saves the contents of this configuration in a file, storing them in
875      * {@link PropertiesConfiguration} format. The platform's default
876      * character set is used.
877      *
878      * @param filename the name of the file
879      * @throws ConfigurationException if an I/O error occurs during saving
880      */
881     public void save(final String filename) throws ConfigurationException {
882         final PropertiesConfiguration outConf = new PropertiesConfiguration();
883         flatten(outConf);
884         outConf.save(filename);
885     }
886 
887     /***
888      * Returns the list of keys contained in this configuration, sorted in
889      * alphabetic order. Delegates to {@link #sortedKeys(boolean)}, setting
890      * <code>inclNotSet</code> to <code>false</code>.
891      *
892      * @return an immutable set containing the sorted list of keys
893      */
894     public SortedSet sortedKeys() {
895         return sortedKeys(false);
896     }
897 
898     /***
899      * Returns the list of keys contained in this configuration, sorted in
900      * alphabetic order.
901      *
902      * @param inclNotSet if <code>true</code>, keys that are not contained in
903      * this configuration but that described in the
904      * {@linkplain #addDescriptorConfig(Configuration) descriptor configuration}
905      * are also included
906      * @return an immutable set containing the sorted list of keys
907      */
908     public SortedSet<Object> sortedKeys(final boolean inclNotSet) {
909         final SortedSet<Object> result = new TreeSet<Object>();
910         final Iterator keys = getKeys();
911 
912         while (keys.hasNext()) {
913             result.add(keys.next());
914         }
915 
916         if (inclNotSet) {
917             // include additional keys from descriptor config
918             final Iterator descKeys = descriptorConfig.getKeys();
919             String key;
920 
921             while (descKeys.hasNext()) {
922                 key = (String) descKeys.next();
923 
924                 // don't re-add key if it is contained in this config or if
925                 // it a prefix of contained keys or if it is a prefix descriptor
926                 // (starting with a dot)
927                 if (!containsKey(key) && super.subset(key).isEmpty()
928                         && (key.indexOf(PROPERTY_DELIM) != 0)) {
929                     result.add(key);
930                 }
931             }
932         }
933 
934         return Collections.unmodifiableSortedSet(result);
935     }
936 
937     /***
938      * Create an Configuration object that is a subset of this one. The new
939      * Configuration object contains every key from the current Configuration
940      * that starts with prefix. The prefix is removed from the keys in the
941      * subset.
942      *
943      * @param prefix The prefix used to select the properties
944      * @return a subset of this configuration; the returned object will be
945      * a {@link TiesConfiguration} instance
946      */
947     public Configuration subset(final String prefix) {
948         return new TiesConfiguration(super.subset(prefix),
949             descriptorConfig.subset(prefix));
950     }
951 
952 }