Line | Hits | Source |
---|---|---|
1 | /* | |
2 | Chrysalis Web Framework [http://chrysalis.sourceforge.net] | |
3 | Copyright (c) 2002, 2003, 2004, Paul Strack | |
4 | ||
5 | All rights reserved. | |
6 | ||
7 | Redistribution and use in source and binary forms, with or without | |
8 | modification, are permitted provided that the following conditions are met: | |
9 | ||
10 | 1. Redistributions of source code must retain the above copyright notice, this | |
11 | list of conditions and the following disclaimer. | |
12 | ||
13 | 2. Redistributions in binary form must reproduce the above copyright notice, | |
14 | this list of conditions and the following disclaimer in the documentation | |
15 | and/or other materials provided with the distribution. | |
16 | ||
17 | 3. Neither the name of the copyright holder nor the names of its contributors | |
18 | may be used to endorse or promote products derived from this software without | |
19 | specific prior written permission. | |
20 | ||
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
31 | */ | |
32 | ||
33 | package org.chwf.config; | |
34 | ||
35 | import java.io.IOException; | |
36 | import java.io.InputStream; | |
37 | import java.util.Collections; | |
38 | import java.util.Locale; | |
39 | import java.util.Map; | |
40 | import java.util.Properties; | |
41 | import java.util.SortedMap; | |
42 | import java.util.TreeMap; | |
43 | import java.util.WeakHashMap; | |
44 | ||
45 | import javax.xml.parsers.SAXParser; | |
46 | import javax.xml.parsers.SAXParserFactory; | |
47 | ||
48 | import org.chwf.util.SequencedHashMap; | |
49 | ||
50 | /** | |
51 | * @author Paul Strack | |
52 | */ | |
53 | 0 | public abstract class ConfigFactory { |
54 | ||
55 | /** Property for location of extra configuration: "extra.xml.config". */ | |
56 | static final String EXTRA_XML_CONFIG = "extra.xml.config"; | |
57 | ||
58 | /** | |
59 | * A map for storing Config objects. This map holds its | |
60 | * contents via weak references so that references can be | |
61 | * deleted if application memory becomes an issue. The | |
62 | * deleted Config object will be reloaded when requested. | |
63 | */ | |
64 | 2 | static final Map CONFIG_MAP = new WeakHashMap(); |
65 | ||
66 | /** Constant for empty string. */ | |
67 | static final String EMPTY_STRING = ""; | |
68 | ||
69 | /** Constant for underscore. */ | |
70 | static final char UNDERSCORE = '_'; | |
71 | ||
72 | /** Character delimiting sub-names in configuration keys. */ | |
73 | static final char KEY_DELIMITER = '.'; | |
74 | ||
75 | /** File extension for properties. */ | |
76 | static final String PROPERTY_FILE_EXTENSION = ".properties"; | |
77 | ||
78 | /** File extension for xml. */ | |
79 | static final String XML_FILE_EXTENSION = ".xml"; | |
80 | ||
81 | /** Base file name for package-level resources. */ | |
82 | static final String PACKAGE_RESOURCE = "chrysalis"; | |
83 | ||
84 | /** Length of package resource name. */ | |
85 | 1 | static final int PACKAGE_RESOURCE_LENGTH = PACKAGE_RESOURCE.length(); |
86 | ||
87 | /** Limit to number of rounds of property expansion. */ | |
88 | static final int EXPANSION_LIMIT = 100; | |
89 | ||
90 | /** String used to mark start of property expansion point. */ | |
91 | static final String EXPANSION_START = "${"; | |
92 | ||
93 | /** String used to mark end of property expansion point. */ | |
94 | static final String EXPANSION_END = "}"; | |
95 | ||
96 | /** File separator abbreviation. */ | |
97 | static final String FILE_SEPARATOR_ABBREVIATION = "/"; | |
98 | ||
99 | /** Path separator abbrevation. */ | |
100 | static final String PATH_SEPARATOR_ABBREVIATION = ":"; | |
101 | ||
102 | /** File separator system property key. */ | |
103 | static final String FILE_SEPARATOR = "file.separator"; | |
104 | ||
105 | /** Path separator system property key. */ | |
106 | static final String PATH_SEPARATOR = "path.separator"; | |
107 | ||
108 | /** Empty sorted map singleton. */ | |
109 | 1 | static final SortedMap EMPTY_SORTED_MAP = |
110 | Collections.unmodifiableSortedMap(new TreeMap()); | |
111 | ||
112 | /** | |
113 | * <p>Factory method for Config class.</p> | |
114 | * | |
115 | * @param contextClass The context class for the configuration. | |
116 | * @return The Config object for the given class. | |
117 | * @throws ConfigurationException For errors in loading configuration data. | |
118 | */ | |
119 | public static Config getConfig(Class contextClass) | |
120 | throws ConfigurationException { | |
121 | 367 | return getConfig(contextClass.getName(), null); |
122 | } | |
123 | ||
124 | /** | |
125 | * <p>Factory method for Config class.</p> | |
126 | * | |
127 | * @param contextClass The context class for the configuration. | |
128 | * @param locale The locale. | |
129 | * @return The Config object for the given class. | |
130 | * @throws ConfigurationException For errors in loading configuration data. | |
131 | */ | |
132 | public static Config getConfig(Class contextClass, Locale locale) | |
133 | throws ConfigurationException { | |
134 | 420 | return getConfig(contextClass.getName(), locale); |
135 | } | |
136 | ||
137 | /** | |
138 | * Factory method for Config class. The resource name should be the location | |
139 | * of the file in the classpath, with "." separating directories rather than | |
140 | * "/". If you use "/" or your operating systems path separator, they will be | |
141 | * replaced by ".". The resource name should omit the file extension | |
142 | * (".properties" or ".xml"). This factory method can be used for resources | |
143 | * that are not associated with any particular class.<p> | |
144 | * | |
145 | * @param resource The name of the resource. | |
146 | * @return The Config object for the given resource. | |
147 | * @throws ConfigurationException For errors in loading configuration data. | |
148 | */ | |
149 | public static Config getConfig(String resource) | |
150 | throws ConfigurationException { | |
151 | 5 | return getConfig(resource, null); |
152 | } | |
153 | ||
154 | /** | |
155 | * Factory method for Config class. As {@link #getConfig(java.lang.String)}, | |
156 | * but include a locale for internationalization.<p> | |
157 | * | |
158 | * @param resource The name of the resource. | |
159 | * @param locale The locale. | |
160 | * @return The Config object for the given resource. | |
161 | * @throws ConfigurationException For errors in loading configuration data. | |
162 | */ | |
163 | public static Config getConfig(String resource, Locale locale) | |
164 | throws ConfigurationException { | |
165 | ||
166 | 4870 | resource = resource.replace('/', '.'); |
167 | 4870 | Config config = null; |
168 | 4870 | String localizedResource = resource + deriveLocaleName(locale); |
169 | ||
170 | // Synchronize on the config map for thread safety | |
171 | 4870 | synchronized (CONFIG_MAP) { |
172 | 4870 | config = (Config) CONFIG_MAP.get(localizedResource); |
173 | 4870 | if (config == null) { |
174 | 3544 | config = new ConfigImpl(resource, locale); |
175 | 3542 | CONFIG_MAP.put(localizedResource, config); |
176 | } | |
177 | 4868 | } |
178 | 4868 | return config; |
179 | } | |
180 | ||
181 | /** | |
182 | * Derive name for Locale object. | |
183 | * | |
184 | * @param locale The locale | |
185 | * @return The name | |
186 | */ | |
187 | static String deriveLocaleName(Locale locale) { | |
188 | 9331 | if (locale == null) { |
189 | 6802 | return EMPTY_STRING; |
190 | } else { | |
191 | 2529 | if ((locale.getCountry() == null) |
192 | || locale.getCountry().equals(EMPTY_STRING)) { | |
193 | 1451 | return UNDERSCORE + locale.getLanguage(); |
194 | } else { | |
195 | 1078 | return UNDERSCORE |
196 | + locale.getLanguage() | |
197 | + UNDERSCORE | |
198 | + locale.getCountry(); | |
199 | } | |
200 | } | |
201 | } | |
202 | ||
203 | /** | |
204 | * Derive parent config (up the package hierarchy) from resource and locale. | |
205 | * | |
206 | * @param resource The resource name. | |
207 | * @param locale The locale. | |
208 | * @return The parent config. | |
209 | */ | |
210 | static Config deriveParentConfig(String resource, Locale locale) { | |
211 | 3525 | if (resource.equals(PACKAGE_RESOURCE)) { |
212 | 534 | return TERMINAL_CONFIG; |
213 | 2991 | } else if (resource.endsWith(PACKAGE_RESOURCE)) { |
214 | 2275 | String parentResource = deriveParentResourceName(resource); |
215 | 2275 | return getConfig(parentResource, locale); |
216 | } else { | |
217 | 716 | String parentResource = stripLastName(resource) + PACKAGE_RESOURCE; |
218 | 716 | return getConfig(parentResource, locale); |
219 | } | |
220 | } | |
221 | ||
222 | /** | |
223 | * Derive config for parent locale from resource and locale. | |
224 | * | |
225 | * @param resource The resource name. | |
226 | * @param locale The locale. | |
227 | * @return The config for parent locale. | |
228 | */ | |
229 | static Config deriveParentLocale(String resource, Locale locale) { | |
230 | 3542 | if (locale == null) { |
231 | 2542 | return TERMINAL_CONFIG; |
232 | 1000 | } else if (locale.getCountry().equals(EMPTY_STRING)) { |
233 | 556 | return getConfig(resource, null); |
234 | 444 | } else if (locale.getVariant().equals(EMPTY_STRING)) { |
235 | 444 | locale = new Locale(locale.getLanguage(), EMPTY_STRING); |
236 | 444 | return getConfig(resource, locale); |
237 | } else { | |
238 | 0 | locale = new Locale(locale.getLanguage(), locale.getVariant()); |
239 | 0 | return getConfig(resource, locale); |
240 | } | |
241 | } | |
242 | ||
243 | /** | |
244 | * Derive parent resource name for package resource (by chopping off the | |
245 | * last value). | |
246 | * | |
247 | * @param resource The resource name. | |
248 | * @return The parent resource name. | |
249 | */ | |
250 | static String deriveParentResourceName(String resource) { | |
251 | 2275 | int length = resource.length() - PACKAGE_RESOURCE_LENGTH - 1; |
252 | 2275 | String stripped = resource.substring(0, length); |
253 | 2275 | return stripLastName(stripped) + PACKAGE_RESOURCE; |
254 | } | |
255 | ||
256 | //----------------------------------------------------------------- | |
257 | // Static initialization methods | |
258 | //----------------------------------------------------------------- | |
259 | ||
260 | /** | |
261 | * Load config data for resource. | |
262 | * | |
263 | * @param resource The resource name. | |
264 | * @return The resource data. | |
265 | */ | |
266 | static Map initData(String resource) { | |
267 | 3544 | Map data = new SequencedHashMap(); |
268 | 3544 | data.putAll(loadPropertyFileData(resource)); |
269 | 3544 | loadXMLFileData(resource, data); |
270 | 3544 | loadExtraXMLData(resource, data); |
271 | 3544 | trimWhitespace(data); |
272 | 3544 | expandProperties(resource, data); |
273 | 3542 | if (data.isEmpty()) { |
274 | 2976 | return Collections.EMPTY_MAP; // Singleton to reduce memory usage |
275 | } else { | |
276 | 566 | return data; |
277 | } | |
278 | } | |
279 | ||
280 | /** | |
281 | * Load config data from property file and inserts it into the map. | |
282 | * | |
283 | * @param resource The resource name. | |
284 | * @return The resource data. | |
285 | */ | |
286 | static Map loadPropertyFileData(String resource) { | |
287 | 3544 | String file = deriveFile(resource, PROPERTY_FILE_EXTENSION); |
288 | 3544 | InputStream is = getStream(file); |
289 | 3544 | if (is != null) { |
290 | 386 | Properties props = new Properties(); |
291 | try { | |
292 | 386 | props.load(is); |
293 | 386 | } catch (IOException ex) { |
294 | 0 | handleFileError(file, ex); |
295 | 0 | } finally { |
296 | 0 | try { |
297 | 386 | is.close(); |
298 | 386 | } catch (IOException ex) { |
299 | 0 | handleFileError(file, ex); |
300 | 386 | } |
301 | } | |
302 | 386 | return props; |
303 | } else { | |
304 | 3158 | return Collections.EMPTY_MAP; |
305 | } | |
306 | } | |
307 | ||
308 | /** | |
309 | * Load config data from xml file and inserts it into the map. | |
310 | * | |
311 | * @param resource The resource name. | |
312 | * @param data The map to which resource data is added. | |
313 | */ | |
314 | static void loadXMLFileData(String resource, Map data) { | |
315 | 3544 | String file = deriveFile(resource, XML_FILE_EXTENSION); |
316 | 3544 | InputStream is = getStream(file); |
317 | 3544 | if (is != null) { |
318 | try { | |
319 | 186 | SAXParser sp = SAXParserFactory.newInstance().newSAXParser(); |
320 | 186 | XMLDigester handler = new XMLDigester(); |
321 | 186 | sp.parse(is, handler); |
322 | 186 | data.putAll(handler.getData()); |
323 | 186 | } catch (Exception ex) { |
324 | 0 | handleFileError(file, ex); |
325 | 0 | } finally { |
326 | 0 | try { |
327 | 186 | is.close(); |
328 | 186 | } catch (IOException ex) { |
329 | 0 | handleFileError(file, ex); |
330 | 186 | } |
331 | } | |
332 | } | |
333 | 3544 | } |
334 | ||
335 | /** | |
336 | * Load extra xml data from special location. | |
337 | * | |
338 | * @param resource The resource name. | |
339 | * @param data The map to which resource data is added. | |
340 | */ | |
341 | static void loadExtraXMLData(String resource, Map data) { | |
342 | 3544 | String file = (String) data.get(EXTRA_XML_CONFIG); |
343 | 3544 | if (file == null) { |
344 | 3528 | return; |
345 | } | |
346 | 16 | InputStream is = getStream(file); |
347 | 16 | if (is != null) { |
348 | try { | |
349 | 16 | SAXParser sp = SAXParserFactory.newInstance().newSAXParser(); |
350 | 16 | XMLDigester handler = new XMLDigester(); |
351 | 16 | sp.parse(is, handler); |
352 | 16 | data.putAll(handler.getData()); |
353 | 16 | } catch (Exception ex) { |
354 | 0 | handleFileError(file, ex); |
355 | 0 | } finally { |
356 | 0 | try { |
357 | 16 | is.close(); |
358 | 16 | } catch (IOException ex) { |
359 | 0 | handleFileError(file, ex); |
360 | 16 | } |
361 | } | |
362 | } | |
363 | 16 | } |
364 | ||
365 | /** | |
366 | * Derive file path from resource name by replace key delimiters | |
367 | * with path delimiters and adding the file extension. | |
368 | * | |
369 | * @param resource The resource name. | |
370 | * @param extension The file extension. | |
371 | * @return The derived file path. | |
372 | */ | |
373 | static String deriveFile(String resource, String extension) { | |
374 | 7088 | return resource.replace(KEY_DELIMITER, '/') + extension; |
375 | } | |
376 | ||
377 | /** | |
378 | * Open input stream to file searching the classpath. | |
379 | * | |
380 | * @param file The relative file path. | |
381 | * @return A stream pointing the to file (or <code>null</code> if not found). | |
382 | */ | |
383 | static InputStream getStream(String file) { | |
384 | 7104 | ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
385 | 7104 | return loader.getResourceAsStream(file); |
386 | } | |
387 | ||
388 | /** | |
389 | * Report file error. | |
390 | * | |
391 | * @param file The relative file path. | |
392 | * @param ex The original exception. | |
393 | * @throws ConfigurationException With information about the error. | |
394 | */ | |
395 | static void handleFileError(String file, Exception ex) | |
396 | throws ConfigurationException { | |
397 | 0 | throw new ConfigurationException( |
398 | ConfigurationException.MESSAGE_CONFIG_FILE_ERROR, | |
399 | new Object[] { file, ex.getMessage()}); | |
400 | } | |
401 | ||
402 | /** | |
403 | * Trim whitespace from data. | |
404 | * | |
405 | * @param data The data. | |
406 | */ | |
407 | static void trimWhitespace(Map data) { | |
408 | 3544 | Object[] keys = data.keySet().toArray(); |
409 | 11629 | for (int i = 0; i < keys.length; i++) { |
410 | 8085 | Object key = keys[i]; |
411 | 8085 | String value = (String) data.get(key); |
412 | 8085 | data.put(key, value.trim()); |
413 | } | |
414 | 3544 | } |
415 | ||
416 | /** | |
417 | * Expand all property values. | |
418 | * | |
419 | * @param resource The resource name. | |
420 | * @param data The map containing the data. | |
421 | * @throws ConfigurationException If there is an expansion overflow. | |
422 | */ | |
423 | static void expandProperties(String resource, Map data) | |
424 | throws ConfigurationException { | |
425 | // Use key array to avoid ConcurrentModificationException | |
426 | 3544 | Object[] keys = data.keySet().toArray(); |
427 | 11627 | for (int i = 0; i < keys.length; i++) { |
428 | 8085 | Object key = keys[i]; |
429 | try { | |
430 | 8085 | String oldValue = (String) data.get(key); |
431 | 8085 | String newValue = expandProperty(oldValue, data, 0); |
432 | 8083 | if (newValue != oldValue) { |
433 | 12 | data.put(key, newValue); |
434 | } | |
435 | 8083 | } catch (StackOverflowError e) { |
436 | 2 | throw new ConfigurationException( |
437 | ConfigurationException.MESSAGE_PROPERTY_EXPANSION_OVERFLOW, | |
438 | new Object[] { resource, key }); | |
439 | } | |
440 | } | |
441 | 3542 | } |
442 | ||
443 | /** | |
444 | * Expand single property value. | |
445 | * | |
446 | * @param value The property value being expanded. | |
447 | * @param data The map containing the data. | |
448 | * @param counter A running count of the expansion recursion depth. | |
449 | * @return The expanded property value. | |
450 | * @throws StackOverflowError If the expansion limit is exceeded. | |
451 | */ | |
452 | static String expandProperty(String value, Map data, int counter) | |
453 | throws StackOverflowError { | |
454 | 17878 | if (counter > EXPANSION_LIMIT) { |
455 | 0 | throw new StackOverflowError(); |
456 | } | |
457 | ||
458 | 17878 | int start = value.indexOf(EXPANSION_START); |
459 | 17878 | if (start < 0) { |
460 | 8083 | return value; |
461 | } | |
462 | 9795 | int end = value.indexOf(EXPANSION_END, start); |
463 | 9795 | if (end < 0) { |
464 | 0 | return value; |
465 | } | |
466 | ||
467 | 9795 | String prefix = value.substring(0, start); |
468 | 9795 | String suffix = value.substring(end + 1); |
469 | 9795 | String key = value.substring(start + 2, end); |
470 | ||
471 | 9795 | if (key.equals(FILE_SEPARATOR_ABBREVIATION)) { |
472 | 4 | key = FILE_SEPARATOR; |
473 | 9791 | } else if (key.equals(PATH_SEPARATOR_ABBREVIATION)) { |
474 | 0 | key = PATH_SEPARATOR; |
475 | } | |
476 | ||
477 | 9795 | String property = (String) data.get(key); |
478 | 9795 | if (property == null) { |
479 | 8 | property = System.getProperty(key); |
480 | } | |
481 | ||
482 | 9795 | value = prefix + property + suffix; |
483 | ||
484 | 9793 | return expandProperty(value, data, counter++); |
485 | } | |
486 | ||
487 | /** | |
488 | * Strip last value from name. | |
489 | * | |
490 | * @param name The name being stripped. | |
491 | * @return The stripped name. | |
492 | */ | |
493 | static String stripLastName(String name) { | |
494 | 12576 | return name.substring(0, name.lastIndexOf(KEY_DELIMITER) + 1); |
495 | } | |
496 | ||
497 | /** | |
498 | * Strip last value and period from name. | |
499 | * | |
500 | * @param name The name being stripped. | |
501 | * @return The stripped name. | |
502 | */ | |
503 | static String stripLastNameAndPeriod(String name) { | |
504 | 27768 | if (name.indexOf(KEY_DELIMITER) < 0) { |
505 | 9261 | return EMPTY_STRING; |
506 | } | |
507 | 18507 | return name.substring(0, name.lastIndexOf(KEY_DELIMITER)); |
508 | } | |
509 | ||
510 | /** Singleton that terminates property search by returning null. */ | |
511 | 1 | static final Config TERMINAL_CONFIG = new ConfigImpl() { |
512 | String getWithContext(String context, String key) { | |
513 | return null; | |
514 | } | |
515 | ||
516 | Map getMapWithContext(String context, String key) { | |
517 | return Collections.EMPTY_MAP; | |
518 | } | |
519 | ||
520 | public String getRawValue(String key) { | |
521 | return null; | |
522 | } | |
523 | ||
524 | public Map getOrderedMap(String key) { | |
525 | return Collections.EMPTY_MAP; | |
526 | } | |
527 | }; | |
528 | } |
this report was generated by version 1.0.5 of jcoverage. |
copyright © 2003, jcoverage ltd. all rights reserved. |