View Javadoc

1   /*
2    * Copyright (C) 2006  Tom Gibara
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17   */
18  package com.tomgibara.pronto.config.impl;
19  
20  import com.tomgibara.pronto.util.Arguments;
21  
22  /**
23   * A key with which configuration properties can be retrieved. Keys are
24   * organised heirarchially as 'dot separated' strings. ConfigKey instances can
25   * be created directly via the constructors on this class, or by using a
26   * ConfigKeyFactory object.
27   * 
28   * Instances of this class, however constructed are safe of concurrent use.
29   * 
30   * @author Tom Gibara
31   */
32  
33  final class ConfigKey implements Comparable, Cloneable {
34  
35      /**
36       * A key whose unique object reference is used to identify that the parent
37       * field requires computation.
38       */
39  
40      private static final ConfigKey UNCOMPUTED_PARENT = new ConfigKey("<UNCOMPUTED>");
41  
42      /**
43       * This string reference is used to identify that the name field requires
44       * computation. The value has been chosen as a short impossible name value.
45       */
46  
47      private static final String UNCOMPUTED_NAME = ".";
48  
49      /**
50       * Checks a path to confirm that it is correctly separated by dots. This
51       * means that there are no leading, trailing or adjacent dots. In addition,
52       * a boolean is returned indicating whether the path is atomic (contains no
53       * dots).
54       * 
55       * This method is intended to be efficient so that keys can be created from
56       * paths cheaply.
57       * 
58       * @param path
59       *            the path being converted into key.
60       * @param name
61       *            the name of the argument being checked
62       * @return true if the path is atomic (contains no dots)
63       * 
64       * @throws IllegalArgumentException
65       *             if the supplied path contains leading, trailing or adjacent
66       *             dots
67       */
68  
69      private static boolean checkPath(final String path, final String name) throws IllegalArgumentException {
70          // check not null or empty
71          Arguments.notEmpty(path, name);
72          // it's convenient (efficient) to check for atomicity of path name at
73          // the same time
74          boolean atomic = true;
75          // check for empty path elements
76          int i = -1;
77          while (true) {
78              int j = path.indexOf('.', i + 1);
79              if (j == 0) throw new IllegalArgumentException(String.format("%s starts with '.' : %s", name, path));
80              if (j == i + 1) throw new IllegalArgumentException(String.format("%s with empty element at index %d: %s",
81                      name, i, path));
82              if (j == -1) {
83                  if (i == path.length() - 1) throw new IllegalArgumentException(String.format("%s ends with '.' : %s",
84                          name, path));
85                  break;
86              } else {
87                  i = j;
88                  atomic = false;
89              }
90          }
91          return atomic;
92      }
93  
94      // fields
95  
96      /**
97       * The complete path which this configuration key represents.
98       */
99  
100     private final String path;
101 
102     /**
103      * The parent key. This field will be null for atomic paths. For keys not
104      * made with a factory, the parent will not be computed until it is
105      * required. Factories eagerly generate the parent keys of all the keys they
106      * generate. This field is volatile to safely allow the parent key to be
107      * lazily computed.
108      */
109 
110     private volatile ConfigKey parent;
111 
112     /**
113      * The name of this key, after it has been computed, this is equal to the
114      * last element of the path (or the whole path if the path is atomic).
115      * Volatility allows lazy loading.
116      */
117 
118     private volatile String name;
119 
120     // constructors
121 
122     /**
123      * Constructs a new key on behalf of a factory. Keys constructed in this way
124      * have their parent eagerly computed.
125      * 
126      * @param factory
127      *            the factory which is creating the key, not null
128      * @param path
129      *            a valid key path, not null
130      * 
131      * @throws IllegalArgumentException
132      *             if the supplied path contains leading, trailing or adjacent
133      *             dots
134      */
135 
136     ConfigKey(final ConfigKeyFactory factory, final String path) throws IllegalArgumentException {
137         boolean atomic = checkPath(path, "path");
138         Arguments.notNull(factory, "factory");
139 
140         this.path = path;
141         if (atomic) {
142             parent = null;
143             name = path;
144         } else {
145             // induce the factory to create the ancestor keys eagerly
146             // this ensures that getParent can be called without synchronization
147             int i = path.lastIndexOf('.');
148             parent = factory.newKey(path.substring(0, i));
149             name = path.substring(i + 1);
150         }
151     }
152 
153     /**
154      * Constructs a new key from the specified path. This is equivalent to
155      * constructing a new key with a null parent key.
156      * 
157      * @param path
158      *            a valid key path, not null
159      * @throws IllegalArgumentException
160      *             if the supplied path contains leading, trailing or adjacent
161      *             dots
162      */
163 
164     public ConfigKey(final String path) throws IllegalArgumentException {
165         boolean atomic = checkPath(path, "path");
166 
167         this.path = path;
168         // optimization - we know that the parent is null (and that path ==
169         // name) if the path is atomic
170         if (atomic) {
171             parent = null;
172             name = path;
173         } else {
174             parent = UNCOMPUTED_PARENT;
175             name = UNCOMPUTED_NAME;
176         }
177     }
178 
179     /**
180      * Constructs a key which is the extension of an existing key. As the
181      * parameter name suggests, the supplied subpath is permitted to include
182      * valid dot separators. If the supplied parent key is null, this
183      * constructor mimics the behaviour of the path-only constructor. Note the
184      * supplied parent is not necessarily equal to the value returned by
185      * getParent().
186      * 
187      * @param parent
188      *            a parent key, possibly null
189      * @param subpath
190      *            a valid key path, not null
191      * @throws IllegalArgumentException
192      *             if the supplied path contains leading, trailing or adjacent
193      *             dots
194      */
195 
196     public ConfigKey(final ConfigKey parent, final String subpath) throws IllegalArgumentException {
197         boolean atomic = checkPath(subpath, "subpath");
198 
199         path = parent == null ? subpath : parent.path + '.' + subpath;
200         // optimization - use parent if subdomain is atomic and set name =
201         // subpath
202         if (atomic) {
203             this.parent = parent;
204             this.name = subpath;
205         } else {
206             this.parent = UNCOMPUTED_PARENT;
207             this.name = UNCOMPUTED_NAME;
208         }
209     }
210 
211     // accessors
212 
213     /**
214      * Returns the parent key of this key. This is generated by removing the
215      * last dot separated element from the key's path. If the path contains no
216      * dots, null is returned.
217      * 
218      * @return The parent key of this key, or null
219      */
220 
221     public ConfigKey getParent() {
222         if (parent == UNCOMPUTED_PARENT) {
223             int i = path.lastIndexOf('.');
224             if (i == -1) {
225                 parent = null;
226             } else {
227                 String parentDomain = path.substring(0, i);
228                 parent = new ConfigKey(parentDomain);
229             }
230         }
231         return parent;
232     }
233 
234     /**
235      * Returns the name of the key. This is the last dot separated element in
236      * the key's path. If the key contains no dots, the whole path is returned.
237      * To obtain the entire key path use the toString() method.
238      * 
239      * @return the name of the key, never null
240      */
241 
242     public String getName() {
243         if (name == UNCOMPUTED_NAME) {
244             int i = path.lastIndexOf('.');
245             if (i == -1) {
246                 name = path;
247             } else {
248                 name = path.substring(i + 1);
249             }
250         }
251         return name;
252     }
253 
254     // object methods
255 
256     /**
257      * Induces an ordering on keys consistent with the default ordering of their
258      * paths as strings.
259      * 
260      * @param obj
261      *            the object to compare with
262      * @return the comparison result
263      */
264 
265     public int compareTo(final Object obj) {
266         ConfigKey that = (ConfigKey) obj;
267         if (this == that) {
268             return 0;
269         } else if (this == null) {
270             return -1;
271         } else if (that == null) {
272             return 1;
273         } else {
274             return this.path.compareTo(that.path);
275         }
276     }
277 
278     /**
279      * Two keys are equal precisely when they have the same path.
280      * 
281      * @param obj
282      *            the object to compare for equality
283      * @return true if the paths are equivalent
284      */
285 
286     public boolean equals(final Object obj) {
287         if (obj == this) return true;
288         if (!(obj instanceof ConfigKey)) return false;
289         ConfigKey that = (ConfigKey) obj;
290         return this.path.equals(that.path);
291     }
292 
293     /**
294      * @return a hashcode consistent with key equality.
295      */
296 
297     public int hashCode() {
298         return path.hashCode();
299     }
300 
301     /**
302      * Returns the path of this key.
303      * 
304      * @return the key's path, never null
305      */
306 
307     public String toString() {
308         return path;
309     }
310 
311 }