1 package org.lsst.ccs.config.utilities;
2
3 import groovy.util.Eval;
4 import org.lsst.gruth.types.TypeUtils;
5 import org.lsst.gruth.types.GArray;
6 import org.lsst.gruth.types.GList;
7 import org.lsst.gruth.types.GMap;
8 import org.lsst.gruth.types.GStruct;
9
10 import java.io.*;
11 import java.net.URISyntaxException;
12 import java.net.URL;
13 import java.util.*;
14 import java.util.prefs.BackingStoreException;
15 import java.util.prefs.Preferences;
16
17 /**
18 * Stores as String various org.lsst.gruth.types in a property store.
19 * <BR/>
20 * The way these properties are stored is abstract: though the usual (and default) way is to use
21 * a file or a "preferences" system, it can be modified to use something else (such as a database).
22 * If you do not explicitly commit your changes to the properties this will done automatically
23 * when the JVM stops (but this delayed commit won't tell you about I/O problems).
24 * <p/>
25 * The type of the stored data is not obvious and should be known the the caller code.
26 * That's why there are different method to store/retrieve data (each method for each type).
27 * <p/>
28 * <p/>
29 * <B>important note</B> : once you have stored some data with a key you cannot re-use this key for another type.
30 * Or if you want to do that then put the property to <TT>null</TT> before.
31 * <p/>
32 * <p/>
33 * <B>Important note</B> : for the time being (this may change later) instances of this class are not garbage collected
34 *
35 * @author bamade
36 */
37 // Date: 08/11/12
38
39 //TODO: recheck all the null values!
40 public class PersistentProperties implements Cloneable {
41 /**
42 * this class defines the way the properties are made persistent and read.
43 * A <TT>Store</TT> could be a properties file or a database for example.
44 */
45 public static interface Store {
46 /**
47 * should be normally part of the constructor: call this method only once!
48 *
49 * @param props
50 */
51 void storeIsFor(PersistentProperties props);
52
53 /**
54 * will save the properties registered for this store
55 *
56 * @throws IOException
57 */
58 void commit() throws IOException;
59
60 /**
61 * will read the Properties registered by this store
62 *
63 * @throws IOException
64 */
65 void update() throws IOException;
66 }
67
68 public static class FileStore implements Store {
69 String storeName;
70 PersistentProperties persistentProperties;
71 File currentFile;
72
73 public void storeIsFor(PersistentProperties props) {
74 if (storeName != null) {
75 throw new IllegalStateException("name already set for Store");
76 }
77 persistentProperties = props;
78 this.storeName = props.name;
79 }
80
81 @Override
82 public void commit() throws IOException {
83 synchronized (persistentProperties) {
84 //System.out.println(" storing " + persistentProperties.properties);
85 checkForFile();
86 try (FileOutputStream fos = new FileOutputStream(currentFile)) {
87 persistentProperties.properties.store(fos, "persistent properties store for " +
88 persistentProperties.name);
89 } catch (IOException exc) {/*IGNORE*/ }
90 persistentProperties.modified = false;
91 }
92 }
93
94 private void checkForFile() throws IOException {
95 if (currentFile == null) {
96 //TODO: change according to lss deployment specifications
97 URL url = ClassLoader.getSystemClassLoader().getResource( "/" + storeName + ".properties");
98 if (url != null) {
99 try {
100 currentFile = new File(url.toURI());
101 } catch (URISyntaxException e) {
102 throw new IOException(e);
103 }
104 } else {
105 currentFile = new File(storeName + ".properties");
106 }
107 }
108
109 }
110
111
112 @Override
113 public void update() throws IOException {
114 synchronized (persistentProperties) {
115 checkForFile();
116 try (FileInputStream fis = new FileInputStream(currentFile)){
117 persistentProperties.properties.load(fis);
118 } catch (IOException exc) {
119 /*IGNORE*/
120 }
121 //is it relevant?
122 persistentProperties.modified = false;
123 }
124 }
125 }
126
127 public static class PreferenceStore implements Store {
128 String storeName;
129 PersistentProperties persistentProperties;
130 Preferences topNode;
131
132 @Override
133 public void storeIsFor(PersistentProperties props) {
134 if (storeName != null) {
135 throw new IllegalStateException("name already set for Store");
136 }
137 persistentProperties = props;
138 this.storeName = props.name;
139 topNode = Preferences.userRoot().node("org.lsst.ccs." + this.storeName);
140 }
141
142 @Override
143 public void commit() throws IOException {
144 synchronized (persistentProperties) {
145 for (Map.Entry<Object, Object> entry : persistentProperties.properties.entrySet()) {
146 String key = (String) entry.getKey();
147 String value = (String) entry.getValue();
148 topNode.put(key, value);
149 }
150 try {
151 topNode.flush();
152 } catch (BackingStoreException e) {
153 throw new IOException(e);
154 }
155 persistentProperties.modified = false;
156 }
157 }
158
159
160 @Override
161 public void update() throws IOException {
162 synchronized (persistentProperties) {
163 try {
164 topNode.sync();
165 for (String key : topNode.keys()) {
166 //default value should not happen here
167 String val = topNode.get(key, "");
168 persistentProperties.properties.put(key, val);
169 }
170 } catch (BackingStoreException e) {
171 throw new IOException(e);
172 }
173 }
174 }
175 }
176
177 ///TODO use SoftReferences and a queue
178 private static ArrayList<PersistentProperties> activeProperties =
179 new ArrayList<PersistentProperties>();
180
181 static {
182 // start a Runtime shutdown hook
183 Runtime.getRuntime().addShutdownHook(new Thread() {
184 public void run() {
185 //read the activeProperties
186 for (PersistentProperties props : activeProperties) {
187 Store store = props.store;
188 if (store != null) {
189 if (props.modified) {
190 try {
191 store.commit();
192 } catch (IOException exc) {
193 //TODO log
194 }
195 }
196 }
197 }
198 }
199 });
200 }
201
202 private Store store;
203 private String name;
204 private Properties properties = new Properties();
205 private volatile boolean modified;
206
207 /**
208 * creates a <TT>PersistenProperties</TT> with a specific store.
209 *
210 * @param name (beware should be consistent with that are legal for your store!)
211 * @param store
212 */
213 public PersistentProperties(String name, Store store) {
214 this.store = store;
215 this.name = name;
216 store.storeIsFor(this);
217 activeProperties.add(this) ;
218 try {
219 store.update();
220 } catch (IOException e) {
221 //ok not present
222 }
223 }
224
225 /**
226 * uses a fileStore.
227 *
228 * @param name (beware should be consistent with a fileName)
229 */
230 public PersistentProperties(String name) {
231 this(name, new FileStore());
232 }
233
234 /**
235 * creates a persistent properties object backed by User preferences.
236 * @param name
237 * @return
238 */
239 public static PersistentProperties preferencesFactory(String name) {
240 return new PersistentProperties(name, new PreferenceStore()) ;
241 }
242
243
244 // private utilities methods
245
246
247 private Integer toInteger(String intString) {
248 if ("null".equals(intString) ) return null;
249 return new Integer(intString);
250 }
251
252 private Double toDouble(String doubleString) {
253 if ("null".equals(doubleString)) return null;
254 return new Double(doubleString);
255 }
256
257 private Float toFloat(String floatString) {
258 if ("null".equals(floatString)) return null;
259 return new Float(floatString);
260 }
261
262 private Boolean toBoolean(String booleanString) {
263 //TODO: return false?
264 if ("null".equals(booleanString)) return null;
265 return new Boolean(booleanString);
266 }
267
268 private Map toMap(String mapString) {
269 if ("null".equals(mapString)) return null;
270 return GMap.valueOf(mapString);
271 }
272
273 private List toList(String listString) {
274 if ("null".equals(listString)) return null;
275 return GList.valueOf(listString);
276 }
277
278 private <T> T toObject(Class<T> clazz, String objectString) {
279 if ("null".equals(objectString)) return null;
280 if (clazz.isArray()) {
281 return (T) GArray.valueOf(objectString, clazz.getName());
282 } else {
283 Object obj = Eval.me(objectString);
284 if (obj instanceof List) {
285 return (T) GStruct.valueOf(clazz, (List) obj);
286 }
287 if (obj instanceof Map) {
288 return (T) GStruct.valueOf(clazz, (Map) obj);
289 }
290 }
291 throw new IllegalArgumentException(" format not available : " + objectString + "for " + clazz);
292 }
293
294 ///////////// methods for simple
295
296 /**
297 * put an Integer value in the Properties
298 *
299 * @param key
300 * @param value
301 * @return the previous Integer value or null
302 */
303 public synchronized Integer putInteger(String key, int value) {
304 Object res = properties.setProperty(key, String.valueOf(value));
305 Integer resInt = toInteger((String) res);
306 modified = true;
307 return resInt;
308 }
309
310
311 /**
312 * get a Integer value from the Properties
313 *
314 * @param key
315 * @return the value or null if value is not set (or set to null)
316 */
317 public Integer getInteger(String key) {
318 return toInteger(properties.getProperty(key));
319 }
320
321 /**
322 * put a Boolean value in the properties
323 *
324 * @param key
325 * @param bool
326 * @return the previous Boolean value or null
327 */
328 public synchronized Boolean putBoolean(String key, boolean bool) {
329 Object res = properties.setProperty(key, String.valueOf(bool));
330 Boolean resBool = toBoolean((String) res);
331 modified = true;
332 return resBool;
333 }
334
335 /**
336 * gets a Boolean value stored with this key
337 *
338 * @param key
339 * @return
340 */
341 public Boolean getBoolean(String key) {
342 return toBoolean(properties.getProperty(key));
343 }
344
345 /**
346 * puts a Double value in the properties
347 *
348 * @param key
349 * @param val
350 * @return previous double value or null
351 */
352 public synchronized Double putDouble(String key, double val) {
353 Object res = properties.setProperty(key, String.valueOf(val));
354 Double resDbl = toDouble((String) res);
355 modified = true;
356 return resDbl;
357 }
358
359 /**
360 * get a double value associated with the key
361 *
362 * @param key
363 * @return value or null if no such property
364 */
365 public Double getDouble(String key) {
366 return toDouble(properties.getProperty(key));
367 }
368
369 /**
370 * registers a Float as a Property
371 *
372 * @param key
373 * @param val
374 * @return previous value or null
375 */
376 public synchronized Float putFloat(String key, float val) {
377 Object res = properties.setProperty(key, String.valueOf(val));
378 Float resDbl = toFloat((String) res);
379 modified = true;
380 return resDbl;
381 }
382
383 /**
384 * gets a Float from a value in the properties
385 *
386 * @param key
387 * @return
388 */
389 public Float getFloat(String key) {
390 return toFloat(properties.getProperty(key));
391 }
392
393 ///////////////// OBJECTS
394
395 private Object rawPutObject(String key, Object obj) {
396 if (obj == null) {
397 return properties.setProperty(key, "null");
398 }
399 Object res = properties.setProperty(key, TypeUtils.stringify(obj));
400 return res;
401 }
402
403 // define String, Array, list, map, struct
404
405 /**
406 * registers a String in the properties
407 *
408 * @param key
409 * @param value
410 * @return previous value or null if there was none
411 */
412 public synchronized String putString(String key, String value) {
413 if(value == null) {value = "null" ; }
414 String res = (String) properties.setProperty(key, value);
415 modified = true;
416 return res;
417 }
418
419 /**
420 * gets a String associated with a key
421 *
422 * @param key
423 * @return value or null if none was found
424 */
425 public String getString(String key) {
426 return properties.getProperty(key);
427 }
428
429 /**
430 * registers an array in the properties.
431 * You'll have to declare an array class which is compatible with the <TT>value</TT>
432 * argument : if the result is not null the returned Object will be of this type.
433 * <p/>
434 * So for example :
435 * <PRE>
436 * Double[] arrDbl = {2.44, 6.66} ;
437 * Number[] arrNumber = myProperties.putArray("valueArray", Number[].class, arrDbl) ;
438 * </PRE>
439 * Doing this:
440 * <PRE>
441 * Number[] arrNumber = myProperties.putArray("valueArray", Double[].class, arrDbl) ;
442 * </PRE>
443 * will work but you may end up with an <TT>ArrayStoreException</TT>
444 * when dealing later with <TT>arrNumber</TT>
445 *
446 * @param key
447 * @param arrayClass such as String[].class
448 * @param value could be an array which is "assignable to"
449 * this array elements should be of a primitive type, wrapper org.lsst.gruth.types, String, BigDecimal or Arrays of arrays of these org.lsst.gruth.types.
450 * Arrays of other org.lsst.gruth.types of Objects are not supported (some "structs" -see below-would work)
451 * @param <T> type of Array with elements of type X
452 * @param <V> array of any type Y that is assignment compatible to X
453 * @return
454 */
455 public synchronized <T, V extends T> T putArray(String key, Class<T> arrayClass, V value) {
456 Object rawres = rawPutObject(key, value);
457 T res = toObject(arrayClass, (String) rawres);
458 modified = true;
459 return res;
460 }
461
462 /**
463 * returns a value stored in the properties as an array.
464 *
465 * @param key
466 * @param arrayClass
467 * @param <T>
468 * @return the array which is represented by the String stored or null if there is none
469 */
470 public <T> T getArray(String key, Class<T> arrayClass) {
471 return toObject(arrayClass, properties.getProperty(key));
472 }
473
474 /**
475 * This will store objects that stick to a convention (and known as <TT>struct</TT>.
476 * The values are stored as a String version of a <TT>List</TT> or of a <TT>Map</TT>
477 * depending on the definition of the Object:
478 * <UL>
479 * <LI/> if the object has a full fledged constructor, then a List will be used
480 * <LI/> if the object is a <TT>bean</TT> with only <I>getters/setters</I>
481 * then a Map will be used (each pair will be : Name, value)
482 * </UL>
483 * In both cases the <TT>toString</TT> method of these object should exactly produce
484 * a List or Map litteral (with the conventions <TT>[1,'world',2.8]</TT> for lists
485 * and <TT>[number:1, name:'world', max:2.8]</TT> for Maps.
486 *
487 * @param key
488 * @param structClass the name of the class that will be used to analyse previous value
489 * @param value any object that sticks to this <TT>struct</TT> convention
490 * @param <T>
491 * @return
492 */
493 public synchronized <T> T putStruct(String key, Class<T> structClass, T value) {
494 Object rawres = rawPutObject(key, value);
495 T res = toObject(structClass, (String) rawres);
496 modified = true;
497 return res;
498 }
499
500 /**
501 * returns an object stored with the <TT>struct</TT> convention from the properties.
502 *
503 * @param key
504 * @param structClass
505 * @param <T>
506 * @return
507 */
508 public <T> T getStruct(String key, Class<T> structClass) {
509 return toObject(structClass, properties.getProperty(key));
510 }
511
512 /**
513 * registers a List to the properties.
514 * Note that the elements of the list can be only wrappers (Double, Integer,...), and
515 * String objects or List of these lists (or Maps of this simple data).
516 * <p/>
517 * <B>Warning</B> : a floating point value stored will be returned as a <TT>BigDecimal</TT>
518 * this may change in future releases (known feature)
519 *
520 * @param key
521 * @param list
522 * @return
523 */
524 public synchronized List putList(String key, List list) {
525 Object rawres = rawPutObject(key, list);
526 List res = toList((String) rawres);
527 modified = true;
528 return res;
529 }
530
531 /**
532 * returns a list stored in the properties.
533 * <p/>
534 * <B>Warning</B> : a floating point value stored will be returned as a <TT>BigDecimal</TT>
535 * this may change in future releases (known feature)
536 *
537 * @param key
538 * @return
539 */
540 public List getList(String key) {
541 return toList(properties.getProperty(key));
542 }
543
544 /**
545 * stores a <TT>Map</TT> as a String in the properties.
546 * Note that the values of the map can be only wrappers (Double, Integer,...), and
547 * String objects (or lists or Maps of such objects).
548 * <p/>
549 * <B>Warning</B> : a floating point value stored will be returned as a <TT>BigDecimal</TT>
550 * this may change in future releases (known feature)
551 *
552 * @param key
553 * @param map
554 * @return
555 */
556 public synchronized Map putMap(String key, Map map) {
557 Object rawres = rawPutObject(key, map);
558 Map res = toMap((String) rawres);
559 modified = true;
560 return res;
561 }
562
563 /**
564 * returns a <TT>Map</TT> stored in the properties.
565 * <B>Warning</B> : a floating point value stored will be returned as a <TT>BigDecimal</TT>
566 * this may change in future releases (known feature)
567 *
568 * @param key
569 * @return
570 */
571 public Map getMap(String key) {
572 return toMap(properties.getProperty(key));
573 }
574
575 private Object evalIt(String str) {
576 String trimStr = str.trim();
577 // if first character is not [ or number then returns the arg
578 char first = trimStr.charAt(0);
579 Object res = str;
580 if (Character.isDigit(first) || first == '[') {
581 res = Eval.me(trimStr);
582 } else if ("true".equalsIgnoreCase(trimStr)) {
583 return new Boolean(true);
584 } else if ("false".equalsIgnoreCase(trimStr)) {
585 return new Boolean(false);
586 // numeric that start with a dot
587 // see if groovy has such a test ....
588 } else if ("null".equals(trimStr)) {
589 return null ;
590 }
591 return res;
592 }
593
594 /**
595 * Puts an object with no type specification in the properties.
596 * Without type specification some unwanted effect might happen when reading
597 * the previous value:
598 * <UL>
599 * <LI/> floating point values will be returned as BigDecimal
600 * <LI/> Strings that start with a numerical character will fire an attempt to
601 * create a number (which may fail if you have a String such as "200x300")
602 * <LI/> String that start with a "[" character will fire an attempt to read a List or
603 * Map (which may fail!)
604 * <LI/> String "true" or "false" will return a Boolean!
605 * <LI/> <TT>struct</TT> objects won't be analysed
606 * <LI/> arrays will be returned as Lists!
607 * </UL>
608 *
609 * @param key
610 * @param obj
611 * @return
612 */
613 public synchronized Object putObject(String key, Object obj) {
614 Object rawres = rawPutObject(key, obj);
615 if (rawres != null) {
616 rawres = evalIt((String) rawres);
617 }
618 modified = true;
619 return rawres;
620 }
621
622 /**
623 * tries to return an object out of a String.
624 * See remarks with method <TT>putObject</TT>
625 *
626 * @param key
627 * @return
628 */
629 public Object getObject(String key) {
630 String rawres = properties.getProperty(key);
631 Object res = null;
632 if (rawres != null) {
633 res = evalIt(rawres);
634 }
635 return res;
636 }
637
638 public Set<Object> getKeys() {
639 return properties.keySet();
640 }
641
642 //TODO: deep copy or not?
643 @Override
644 public PersistentProperties clone() {
645 PersistentProperties res = null;
646 try {
647 res = (PersistentProperties) super.clone() ;
648 } catch (CloneNotSupportedException e) {
649 assert false : "clone should not fail in PersistentProperties" ;
650 }
651 return res ;
652 }
653 }