View Javadoc

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 }