1
2
3
4
5
6
7
8
9
10 package org.astrogrid.config;
11
12 import java.util.*;
13 import javax.naming.*;
14
15 import java.io.File;
16 import java.io.FileNotFoundException;
17 import java.io.IOException;
18 import java.io.PrintWriter;
19 import java.io.Writer;
20 import java.net.MalformedURLException;
21 import java.net.URL;
22 import java.security.AccessControlException;
23
24 /***
25 * All Things To All Men Configuration.
26 * <p>
27 * A comprehensive facade singleton provding fallback access to Jndi and
28 * standard text configuration files.
29 * <p>
30 * The fallback works like this:
31 * <ul>
32 * <li> Look in local cache (so calling 'setProperty' will override file properties)
33 * <li> Look in jndi for the property.
34 * <li> If that fails, then look in the configuration file (see below for how this is located)
35 * <li> If that fails, then look in the system environments.
36 * <li> If that fails, throw an exception unless a default has been supplied.
37 * </ul>
38 * <p>
39 * The configuration file locator works like this:
40 * <ul>
41 * <li> look in jndi for the key "org.astrogrid.config.url" which gives the url to the file
42 * <li> look in jndi for the key "org.astrogrid.config" which gives the property filename
43 * <li> if that fails, look in the system environment vars for the same key
44 * <li> if that fails, look for the file "astrogrid.properties" on the classpath (not yet implemented)
45 * <li> if that fials, look for the file "astrogrid.properties" in the working directory
46 * </ul>
47 * <p>
48 * The configuration file lookup stops at the first find, so we don't get *too* confused
49 * with lots of configuration files around, and properties being found in one
50 * but not others. If at any point it is referred to (ie JNDI key for the config filename exists) but
51 * there is a problem loading the file, it fails. This means you can be sure that
52 * if you've *tried* to configure it, you will know if it hasn't worked.
53 * <p>
54 * Initialisation is 'lazy' - particularly as we may not want to go looking for
55 * configuration files if everything is in Jndi. However given the dangers of
56 * double-checked locking, the initialisation routines are synchronised and
57 * checked *within* for the initialisation flag
58 * <p>
59 * Failures are *all* reported as exceptions, unless a default is given. So
60 * if you think a value might be missing and you don't want your app to fallover,
61 * supply a default.
62 * <p>
63 * We could think about adding resource bundles instead of property files, but
64 * I think JNDI provides all the complexity we need if we need that much...?
65 * <p>
66 * @author mch
67 */
68
69
70 public class FailbackConfig extends Config {
71
72 /*** Cache - only used for setProperty at the moment, so that Jndi can
73 * reload on-the-fly as necessary */
74 private Hashtable cache = new Hashtable();
75
76 /*** initialised flags */
77 private boolean jndiInitialised = false;
78 private boolean fileInitialised = false;
79
80 /*** Jndi context */
81 private InitialContext jndiContext = null;
82
83 /*** Properties file context */
84 private Properties properties = null;
85
86 /*** JNDI key to url that locates the properties file */
87 private static String propertyUrlKey = "org.astrogrid.config.url";
88
89 /*** JNDI key to properties file */
90 private static String propertyKey = "org.astrogrid.config.filename";
91
92 /*** prefix to keys when accessing JNDI services. No idea why this is required... */
93 private static String jndiPrefix = "java:comp/env/";
94
95 /*** Usual filename for properties file in classpath, etc */
96 private static String configFilename = "astrogrid.properties";
97
98 /*** filename for default properties file that come with the distribution */
99 private static String defaultFilename = "default.properties";
100
101 /***
102 * Protected constructor because we shouldn't be able to make one of these.
103 * At the moment the given context is ignored
104 */
105 protected FailbackConfig(Object context) {
106 }
107
108 /***
109 * Call this before the first getProperty if you want to use a different
110 * property file (eg for testing). Throws an exception if properties
111 * already loaded, as it's too late then
112 */
113 public void setConfigFilename(String newName)
114 {
115 if (fileInitialised) {
116 throw new ConfigException("Configuration already initialised - too late to set config filename",null);
117 }
118 configFilename = newName;
119 }
120
121
122 /***
123 * Initialise Jndi access. Note that the context may be null (if there is
124 * no Jndi service) even after initialisation. Synchronized for thread safety
125 */
126 private synchronized void initialiseJndi() {
127
128 if (!jndiInitialised) {
129
130 jndiInitialised = true;
131
132 try {
133 jndiContext = new InitialContext();
134
135 try {
136 jndiContext.lookup("java:comp/env");
137
138 addLoadedFrom("JNDI");
139 }
140 catch (NameNotFoundException nnfe) {
141
142 log.info("Config access to JNDI initialised ("+jndiContext+")");
143 }
144 catch (ServiceUnavailableException sue) {
145
146 log.debug("No JNDI service found ("+sue+") so will not use JNDI for config...");
147 jndiContext = null;
148 }
149 catch (NoInitialContextException nice) {
150
151 log.debug("No JNDI service found ("+nice+") so will not use JNDI for config...");
152 jndiContext = null;
153 }
154 catch (NamingException ne) {
155
156 throw new ConfigException("Initialising Jndi Access", ne);
157 }
158 }
159 }
160
161 /***
162 * Initialise access to properties file. Looks first in jndi for url to file,
163 * then system environment, then classpath, finally working directory.
164 */
165 private synchronized void initialiseFile() {
166
167 if (!fileInitialised) {
168
169 fileInitialised = true;
170
171 URL fileUrl = null;
172
173 String urlValue = null;
174 String filenameValue = null;
175
176
177 if (jndiContext != null) {
178 String jndiUrlKey = jndiPrefix+propertyUrlKey;
179 String jndiFileKey = jndiPrefix+propertyKey;
180 String keyUsed = jndiUrlKey;
181
182 try {
183 try {
184 urlValue = jndiContext.lookup(jndiUrlKey).toString().trim();
185 fileUrl = new URL(urlValue);
186 } catch (NameNotFoundException nnfe) { }
187 log.debug("Config: JNDI key "+jndiUrlKey+" => "+fileUrl);
188 try {
189 filenameValue = jndiContext.lookup(jndiFileKey).toString().trim();
190 } catch (NameNotFoundException nnfe) { }
191 log.debug("Config: JNDI key "+jndiFileKey+" => "+filenameValue);
192
193
194
195 if ((fileUrl != null) && (filenameValue != null)) {
196 throw new ConfigException("Both "+jndiUrlKey+" and "+jndiFileKey+" defined in JNDI; specify only one");
197 }
198
199
200 if (filenameValue != null) {
201 File propertyFile = new File(filenameValue);
202
203 if (!propertyFile.isAbsolute()) {
204 if (lookForConfigFile(filenameValue) == true)
205 {
206
207 return;
208 }
209 throw new FileNotFoundException(filenameValue);
210 }
211
212
213 fileUrl = propertyFile.toURL();
214 keyUsed = jndiFileKey;
215 }
216
217 if (fileUrl != null) {
218 loadFromUrl(fileUrl);
219
220 log.info("Configuration file loaded from '"+fileUrl.toString()+"' (from JNDI Key="+keyUsed+")");
221
222 return;
223 }
224 }
225 catch (MalformedURLException mue) {
226 throw new ConfigException("Configuration file url ("+urlValue+") given in JNDI (key="+jndiUrlKey+") is malformed",mue);
227 }
228 catch (FileNotFoundException fnfe) {
229 throw new ConfigException("Configuration file ("+filenameValue+") given in JNDI (key="+jndiFileKey+") cannot be found",fnfe);
230 }
231 catch (NamingException ne) {
232 throw new ConfigException("Using key '"+jndiUrlKey+"' or '"+jndiFileKey+"' in JNDI gave: ", ne);
233 }
234 catch (IOException ioe) {
235 throw new ConfigException(ioe+" loading property file at '"+fileUrl+
236 "' (returned by JNDI key "+keyUsed+")", ioe);
237 }
238 }
239
240
241 if ((filenameValue == null) && (urlValue == null)) {
242 String sysEnvKey = propertyUrlKey;
243 String sysEnvUrl = System.getProperty(sysEnvKey);
244 log.debug("Config: Sys Env key "+sysEnvKey+" => "+sysEnvUrl);
245 if (sysEnvUrl != null) {
246 try {
247 fileUrl = new URL(sysEnvUrl);
248
249 loadFromUrl(fileUrl);
250
251 log.debug("Configuration file loaded from '"+fileUrl.toString()+"' (from SYS ENV="+sysEnvKey+")");
252
253 return;
254 }
255 catch (MalformedURLException mue) {
256 throw new ConfigException("Configuration file url given in system environment variable '"+
257 sysEnvKey+"' is malformed",mue);
258 }
259 catch (IOException ioe) {
260 throw new ConfigException(ioe+" loading property file at '"+fileUrl+
261 "' (returned by system environment variable '"+sysEnvKey+"')", ioe);
262 }
263 }
264 }
265
266
267 log.info("Config: No key to config file found in JNDI/SysEnv, so falling back to "+configFilename);
268
269 try {
270 if (lookForConfigFile(configFilename)) {
271 return;
272 }
273 }
274 catch (AccessControlException ace) {
275
276
277
278
279 log.error("No Permission to access "+configFilename, ace);
280 }
281
282
283
284 try {
285 if (lookForConfigFile(defaultFilename)) {
286 return;
287 }
288 }
289 catch (AccessControlException ace) {
290
291
292
293
294 log.error("No Permission to access "+defaultFilename, ace);
295 }
296
297
298
299
300 log.warn("No configuration file found; if you need one, "+
301 "make sure "+configFilename+" or "+defaultFilename+" is in your classpath, "+
302 "or set the JNDI key "+propertyUrlKey+" to its URL, "+
303 "or set the JNDI key "+propertyKey+" to its file location");
304 }
305 }
306
307 /***
308 * Looks for given config filename absolutely or in classpath and working directory, and loads
309 * it if found. Returns false if not found.
310 * This could probably make use of Config.resolveFile()
311 */
312 private boolean lookForConfigFile(String givenFilename) {
313
314
315 String filename = resolveEnvironmentVariables(givenFilename);
316
317
318 if (!filename.equals(givenFilename)) {
319 givenFilename = givenFilename + " => "+filename;
320 }
321
322 log.debug("Looking for "+givenFilename);
323
324
325 File f = new File(filename);
326 if (f.isAbsolute()) {
327 if (f.exists()) {
328 loadFromFile(f);
329 return true;
330 }
331 return false;
332 }
333
334
335
336
337 log.debug("Looking for "+givenFilename+" on classpath");
338
339 URL configUrl = this.getClass().getClassLoader().getResource(filename);
340 if (configUrl != null) {
341 try {
342 loadFromUrl(configUrl);
343
344 log.info("Configuration file loaded from '"+configUrl+"' (found in classpath)");
345
346 return true;
347
348 } catch (IOException ioe) {
349 throw new ConfigException(ioe+" loading property file at '"+configUrl+"' (from classpath)", ioe);
350 }
351 }
352
353
354
355 log.debug("Looking for "+givenFilename+" in working directory");
356 if (f.exists()) {
357 loadFromFile(f);
358 return true;
359 }
360
361 return false;
362 }
363
364 private void loadFromFile(File f) {
365 try {
366 loadFromUrl(f.toURL());
367 log.info("Configuration file loaded from '"+f.getAbsoluteFile()+"'");
368 return;
369 }
370 catch (IOException ioe) {
371 throw new ConfigException(ioe+" loading property file at '"+f.getAbsoluteFile(), ioe);
372 }
373 }
374
375
376
377 /***
378 * Loads the properties from the given stream. While this should only be
379 * called once during part of the normal initialisation process, we also
380 * allow public access so that test harnesses etc can load their own
381 * properties differently.
382 */
383 public synchronized void loadFromUrl(URL url) throws IOException {
384 if (properties == null) {
385 properties = new Properties();
386 }
387
388
389
390 Properties localProperties = new Properties();
391 localProperties.load(url.openStream());
392
393 addLoadedFrom(url.toString());
394
395
396
397 String includeFile = localProperties.getProperty("include.config.filename") ;
398 if (includeFile != null) {
399 boolean found = lookForConfigFile(includeFile);
400 if (!found) {
401 throw new ConfigException("include config file '"+includeFile+"' not found");
402 }
403 }
404
405
406 properties.putAll(localProperties);
407
408 }
409
410 /***
411 * Keys in the current JDK Property implementation must not contain whitespace,
412 * colons or equals
413 */
414 public static void assertKeyValid(String key)
415 {
416 assert key.indexOf(":") == -1 : "Key '"+key+"' contains an illegal character - a colon";
417 assert key.indexOf(" ") == -1 : "Key '"+key+"' contains an illegal character - a space";
418 assert key.indexOf("=") == -1 : "Key '"+key+"' contains an illegal character - an equals sign";
419 }
420
421 /***
422 * General get property. Throws exception if property not found in Jndi,
423 * then configuration file, then system environment.
424 */
425 public Object getProperty(String key) {
426
427 assertKeyValid(key);
428
429 String lookedIn = "Cache";
430
431
432 if (cache.containsKey(key)) {
433 return cache.get(key);
434 }
435
436
437 try {
438 if (!jndiInitialised) { initialiseJndi(); }
439
440 if (jndiContext != null) {
441 lookedIn = lookedIn + ", JNDI";
442 return jndiContext.lookup(jndiPrefix+key);
443 }
444 else {
445 lookedIn = lookedIn + ", (No JNDI)";
446 }
447 }
448 catch (NameNotFoundException nnfe) { }
449 catch (NamingException ne) {
450 throw new ConfigException(ne+" locating key="+key+" in JNDI", ne);
451 }
452
453
454 if (!fileInitialised) { initialiseFile(); }
455
456 if (properties == null) {
457 lookedIn = lookedIn +", (no config file)";
458 }
459 else {
460 String value = properties.getProperty(key);
461 lookedIn = lookedIn +", config file(s) ("+loadedFrom()+")";
462
463 if (value != null) {
464 return value;
465 }
466 }
467
468
469 lookedIn = lookedIn +", sysenv";
470 String value = System.getProperty(key);
471 if (value != null) {
472 return value;
473 }
474
475 throw new PropertyNotFoundException("Could not find '"+key+"' in: "+lookedIn);
476 }
477
478
479 /***
480 * Set property. Stores in cache so it overrides all other properties
481 * with the same key.
482 */
483 public void setProperty(String key, Object value) {
484
485
486 if (value == null) {
487 if (cache.containsKey(key)) {
488 cache.remove(key);
489 }
490 } else {
491 cache.put(key, value);
492 }
493 }
494
495 /***
496 * Returns array of values for the given key. Throws exception if property not found in Jndi,
497 * then configuration file, then system environment.
498 */
499 public Object[] getProperties(String key) {
500
501 assertKeyValid(key);
502
503 String lookedIn = "Cache";
504
505
506 if (cache.containsKey(key)) {
507 Object o = cache.get(key);
508 if (o instanceof Object[]) {
509 return (Object[]) o;
510 }
511 else {
512 return new Object[] { o };
513 }
514 }
515
516
517 try {
518 if (!jndiInitialised) { initialiseJndi(); }
519
520 if (jndiContext != null) {
521 lookedIn = lookedIn + ", JNDI";
522 Context javacontext = (Context)jndiContext.lookup(jndiPrefix);
523 NamingEnumeration en = jndiContext.list(jndiPrefix+key);
524 Vector values = new Vector();
525 while (en.hasMoreElements()) {
526 NameClassPair pair = (NameClassPair) en.next();
527 String value = null;
528
529 try {
530 value = javacontext.lookup(pair.getName()).toString();
531 } catch (NamingException ne) {
532 value="??Failed lookup: "+ne;
533 }
534 values.add(value);
535 }
536 return values.toArray();
537 }
538 else {
539 lookedIn = lookedIn + ", (No JNDI)";
540 }
541 }
542 catch (NameNotFoundException nnfe) { }
543 catch (NamingException ne) {
544 throw new ConfigException(ne+" locating key="+key+" in JNDI", ne);
545 }
546
547
548
549
550 if (!fileInitialised) { initialiseFile(); }
551
552 if (properties == null) {
553 lookedIn = lookedIn +", (no config file)";
554 }
555 else {
556 lookedIn = lookedIn +", config file(s) ("+loadedFrom()+")";
557
558 String value = properties.getProperty(key);
559 String value1 = properties.getProperty(key+".1");
560
561
562 if ((value != null) && (value1 != null)) {
563 throw new ConfigException("Both single value and sets of values defined for key "+key+" in property file");
564 }
565
566
567 if (value != null) {
568 return new Object[] { value };
569 }
570
571 if (value1 != null) {
572 Vector values = new Vector();
573 int v = 2;
574 while (value1 != null) {
575 values.add(value1);
576 value1 = properties.getProperty(key+"."+v);
577 v++;
578 }
579 return values.toArray();
580 }
581
582 }
583
584
585 lookedIn = lookedIn +", sysenv";
586 String value = System.getProperty(key);
587 if (value != null) {
588 return new Object[] { value };
589 }
590
591 throw new PropertyNotFoundException("Could not find '"+key+"' in: "+lookedIn);
592 }
593
594 /***
595 * Set property to array. Stores in cache so it overrides all other properties
596 * with the same key.
597 */
598 public void setProperties(String key, Object[] values) {
599 if (values == null) {
600 if (cache.containsKey(key)) {
601 cache.remove(key);
602 }
603 } else {
604 cache.put(key, values);
605 }
606 }
607
608 /***
609 * Returns a list of keys. This list is made up of the values in the
610 * cache, JNDI, properties file and system environment keys; note that duplicate
611 * keys will be hidden.
612 */
613 public Set keySet() {
614
615 if (!jndiInitialised) { initialiseJndi(); }
616 if (!fileInitialised) { initialiseFile(); }
617
618 Set allKeys = new HashSet();
619
620
621 allKeys.addAll(cache.keySet());
622
623
624 if (jndiContext != null) {
625 try {
626 Hashtable jndi = jndiContext.getEnvironment();
627 allKeys.addAll(jndi.keySet());
628 }
629 catch (NamingException ne) {
630 throw new ConfigException("Getting Environment from "+jndiContext,ne);
631 }
632 }
633
634
635 if (properties != null) {
636 allKeys.addAll(properties.keySet());
637 }
638
639
640 allKeys.addAll(System.getProperties().keySet());
641
642 return allKeys;
643 }
644
645 /***
646 * Dumps config contents
647 */
648 public void dumpConfig(Writer writer) {
649
650 PrintWriter out = new PrintWriter(writer);
651
652 out.println("Configuration loaded from: "+loadedFrom());
653 out.println();
654
655
656 if (cache.isEmpty()) {
657 out.println("(Cache is empty)");
658 }
659 else {
660 out.println("Cache:");
661 Enumeration c = cache.keys();
662 while (c.hasMoreElements()) {
663 Object key = c.nextElement();
664 out.println(formKeyValue(key, cache.get(key)));
665 }
666 }
667
668
669 out.println();
670 if (jndiContext != null) {
671 out.println("JNDI:");
672 try {
673 out.println("JNDI Environment:");
674 Hashtable env = jndiContext.getEnvironment();
675 Enumeration j = env.keys();
676 while (j.hasMoreElements()) {
677 Object key = j.nextElement();
678 out.println(formKeyValue(key, env.get(key)));
679 }
680 out.println("JNDI Names:");
681 Context javacontext = (Context)jndiContext.lookup(jndiPrefix);
682 NamingEnumeration n = jndiContext.list(jndiPrefix);
683
684 while (n.hasMoreElements()) {
685 NameClassPair key = (NameClassPair)n.next();
686 String value = "??Failed lookup";
687
688 try {
689 value = javacontext.lookup(key.getName()).toString();
690 } catch (NamingException ne) { }
691 out.println(formKeyValue(key, value));
692 }
693
694 }
695 catch (NamingException ne) {
696 ne.printStackTrace(out);
697 }
698
699 }
700 else {
701 out.println("(No JNDI)");
702 }
703 out.println();
704
705
706 if (properties != null) {
707 out.println("Properties from file(s):");
708 Enumeration p = properties.keys();
709 while (p.hasMoreElements()) {
710 Object key = p.nextElement();
711 out.println(formKeyValue(key, properties.getProperty(key.toString())));
712 }
713 }
714 else {
715 out.println("(No Config File)");
716 }
717 out.println();
718
719
720 out.println("System Environment Variables:");
721 try {
722 Enumeration s = System.getProperties().keys();
723 while (s.hasMoreElements()) {
724 Object key = s.nextElement();
725 out.println(formKeyValue(key, System.getProperty(key.toString())));
726 }
727 }
728 catch (AccessControlException ace) {
729
730 out.println("No blanket access permitted: "+ace);
731 }
732
733 out.flush();
734 }
735
736 /***
737 * Formats a key/value pair for printing. Used by dumpConfig. Does noddy
738 * check for 'password' in the key string and hides value if present
739 */
740 public String formKeyValue(Object key, Object value) {
741 if (key.toString().toLowerCase().indexOf("password") > -1) {
742 return " "+key+" = <hidden>";
743 } else {
744 return " "+key+" = "+value;
745 }
746 }
747
748 /***
749 * Main method dumps config contents to console - useful for debugging
750 */
751 public static void main(String[] args) {
752 FailbackConfig config = new FailbackConfig(null);
753
754
755 config.getProperty("Nuffink", null);
756
757 config.dumpConfig(new PrintWriter(System.out));
758 }
759 }
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879