1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
71 Arguments.notEmpty(path, name);
72
73
74 boolean atomic = true;
75
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
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
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
146
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
169
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
201
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
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
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 }