View Javadoc
1   package com.github.celldynamics.quimp;
2   
3   import java.io.File;
4   import java.io.FileNotFoundException;
5   import java.io.FileReader;
6   import java.io.IOException;
7   import java.io.PrintWriter;
8   import java.io.Reader;
9   import java.io.StringReader;
10  import java.lang.reflect.ParameterizedType;
11  import java.lang.reflect.Type;
12  import java.text.SimpleDateFormat;
13  import java.util.ArrayList;
14  import java.util.Date;
15  
16  import org.slf4j.Logger;
17  import org.slf4j.LoggerFactory;
18  
19  import com.github.celldynamics.quimp.filesystem.IQuimpSerialize;
20  import com.github.celldynamics.quimp.filesystem.versions.IQconfOlderConverter;
21  import com.google.gson.Gson;
22  import com.google.gson.GsonBuilder;
23  import com.google.gson.JsonIOException;
24  import com.google.gson.JsonSyntaxException;
25  
26  /**
27   * Support saving and loading wrapped class to/from JSON file or string.
28   * 
29   * <p>The Serializer class wraps provided objects and converts it to Gson together with itself.
30   * Serializer adds fields like wrapped class name and versioning data (@link {@link QuimpVersion})
31   * to JSON.
32   * 
33   * <p>Restored object is constructed using its constructor. If JSON file does not contain variable
34   * available in class being restored, it will have the value assigned in constructor or null. GSon
35   * overrides variables after they have been created in normal process of object building. Check
36   * {@link #fromReader(Reader)} for details.
37   * 
38   * <p>This serializer accepts only classes derived from IQuimpSerialize interface. Saved class is
39   * packed in top level structure that contains version of software and wrapped class name. Exemplary
40   * use case: SerializerTest#testLoad_1()
41   * 
42   * <p>There is option to skip call afterSerialzie() or beforeSerialzie() method on class restoring
43   * or saving To do so set {@link #doAfterSerialize} to false or {@link #doBeforeSerialize}
44   * 
45   * <p>Serializer supports <tt>Since, Until</tt> tags from GSon library. User can write his own
46   * converters executed if specified condition is met. Serializer compares version of callee tool
47   * (provided in Serializer constructor) with trigger version returned by converter
48   * {@link IQconfOlderConverter} and executes conversion provided by it.
49   * <p>
50   * <b>Important</b>: Until and Since tags are resolved using version of QCONF provided from callee
51   * on json saving or by version read from QCONF file on its loading. Version read from JSON is also
52   * used to decide whether apply converter or not but on load only.
53   * </p>
54   * 
55   * @author p.baniukiewicz
56   * @param <T> class type to be serialised
57   * @see <a href=
58   *      "link">http://stackoverflow.com/questions/14139437/java-type-generic-as-argument-for-gson</a>
59   * @see com.github.celldynamics.quimp.Serializer#registerInstanceCreator(Class, Object)
60   * @see #registerConverter(IQconfOlderConverter)
61   */
62  public class Serializer<T extends IQuimpSerialize> implements ParameterizedType {
63  
64    /**
65     * The Constant LOGGER.
66     */
67    static final Logger LOGGER = LoggerFactory.getLogger(Serializer.class.getName());
68  
69    /**
70     * The gson builder.
71     */
72    public transient GsonBuilder gsonBuilder;
73    private transient Type type;
74  
75    /**
76     * Indicates if afterSerialze should be called.
77     */
78    protected transient boolean doAfterSerialize;
79    /**
80     * Indicates if {@link IQuimpSerialize#beforeSerialize()} should be called.
81     * 
82     * <p>Rather for tests as {@link IQuimpSerialize#beforeSerialize()} is always required.
83     */
84    protected transient boolean doBeforeSerialize;
85  
86    /**
87     * Name of wrapped class, decoded from object.
88     */
89    public String className;
90  
91    /**
92     * Version and other information passed to serializer. Since(17.0202)
93     */
94    public QuimpVersion timeStamp;
95  
96    /**
97     * Date when file has been created.
98     */
99    public String createdOn;
100 
101   /**
102    * Wrapped object being serialized.
103    */
104   public T obj;
105 
106   /**
107    * Version stored in QCONF file loaded by Serialiser.
108    * 
109    * <p>If class is serialised (saved) it contains version provided with constructor. This version
110    * is
111    * provided to GSon on loading json
112    */
113   private transient Double qconfVersionToLoad;
114 
115   /**
116    * Version provided form callee.
117    * 
118    * <p>This version is provided to GSon on saving json.
119    */
120   private transient Double qconfVersionToSave;
121 
122   /**
123    * List of format converters called on every load when certain condition is met.
124    */
125   private transient ArrayList<IQconfOlderConverter<T>> converters = new ArrayList<>();
126 
127   /**
128    * Default constructor used for restoring object.
129    * 
130    * <p>Template T can not be restored during runtime thus the type of wrapped object is not known
131    * for GSon. This is why this type must be passed explicitly to Serializer.
132    * 
133    * @param type class type
134    * @param version Version of framework this class is called from.
135    */
136   public Serializer(final Type type, final QuimpVersion version) {
137     doAfterSerialize = true; // by default use afterSerialize methods to restore object state
138     doBeforeSerialize = true;
139     gsonBuilder = new GsonBuilder();
140     obj = null;
141     this.timeStamp = version;
142     this.type = type;
143     // fill date of creation
144     createdOn = getCurrentDate();
145   }
146 
147   /**
148    * Constructor used for saving wrapped class.
149    * 
150    * @param obj Object being saved
151    * @param version Version of framework this class is called from.
152    */
153   public Serializer(final T obj, final QuimpVersion version) {
154     doAfterSerialize = true; // by default use afterSerialize methods to restore object state
155     doBeforeSerialize = true;
156     gsonBuilder = new GsonBuilder();
157     this.type = obj.getClass();
158     this.obj = obj;
159     className = obj.getClass().getSimpleName();
160     this.timeStamp = version;
161     // set it as callee version if we will save json (json version is read on load only)
162     this.qconfVersionToSave = convertStringVersion(version.getVersion());
163     // fill date of creation
164     createdOn = getCurrentDate();
165   }
166 
167   /**
168    * Save wrapped object passed in constructor as JSON file.
169    * 
170    * <p>Calls {@link IQuimpSerialize#beforeSerialize()} before save.
171    * 
172    * @param filename Name of file
173    * @throws FileNotFoundException if problem with saving
174    * @see com.github.celldynamics.quimp.Serializer#setPretty()
175    * @see com.github.celldynamics.quimp.Serializer#Serializer(IQuimpSerialize, QuimpVersion)
176    * @see com.github.celldynamics.quimp.Serializer#toString()
177    */
178   public void save(final String filename) throws FileNotFoundException {
179     String str;
180     str = toString(); // produce json
181     LOGGER.debug("Saving at: " + filename);
182     PrintWriter f;
183     f = new PrintWriter(new File(filename));
184     f.print(str);
185     f.close();
186   }
187 
188   /**
189    * Load GSon file.
190    * 
191    * @param filename to load
192    * @return Serializer object
193    * @throws IOException when file can not be found
194    * @throws JsonSyntaxException on wrong syntax
195    * @throws JsonIOException on wrong syntax
196    * @throws Exception any other case
197    * @see #load(File)
198    */
199   public Serializer<T> load(final String filename)
200           throws IOException, JsonSyntaxException, JsonIOException, Exception {
201     File file = new File(filename);
202     return load(file);
203   }
204 
205   /**
206    * Load wrapped object from JSON file.
207    * 
208    * <p>Calls {@link IQuimpSerialize#afterSerialize()} after load. The general steps taken on GSon
209    * load are as follows:
210    * 
211    * <p><img src="doc-files/Serializer_1_UML.png"/>
212    * </p>
213    * 
214    * @param filename to load
215    * @return Serialiser object
216    * @throws IOException when file can not be found
217    * @throws JsonSyntaxException on wrong syntax
218    * @throws JsonIOException on wrong syntax
219    * @throws Exception any other case
220    * @see #fromReader(Reader)
221    */
222   public Serializer<T> load(final File filename)
223           throws IOException, JsonSyntaxException, JsonIOException, Exception {
224     LOGGER.debug("Loading from: " + filename.getPath());
225     // gather version from JSON
226     FileReader vr = new FileReader(filename);
227     qconfVersionToLoad = getQconfVersion(vr);
228     vr.close(); // on duplicate to avoid problems with moving pointer
229 
230     FileReader f = new FileReader(filename);
231     return fromReader(f);
232   }
233 
234   /**
235    * Restore wrapped object from JSON string.
236    * 
237    * @param json string with json
238    * @return Serialise object
239    * @throws JsonSyntaxException on wrong syntax
240    * @throws JsonIOException on wrong syntax
241    * @throws Exception any other case
242    * @see #fromReader(Reader)
243    */
244   public Serializer<T> fromString(final String json)
245           throws JsonSyntaxException, JsonIOException, Exception {
246     LOGGER.debug("Reading from string");
247     // gather version from JSON
248     Reader vr = new StringReader(json);
249     qconfVersionToLoad = getQconfVersion(vr);
250     vr.close(); // on duplicate to avoid problems with moving pointer
251 
252     Reader reader = new StringReader(json);
253     return fromReader(reader);
254   }
255 
256   /**
257    * Restore wrapped object from JSON string.
258    * 
259    * @param reader reader that provides JSon string
260    * @see #load(File)
261    * 
262    * @return New instance of loaded object packed in Serializer class. returned instance has
263    *         proper (no nulls or empty strings) fields: className, createdOn, version
264    *         (and its subfields, obj)
265    * @throws Exception from afterSerialize() method (specific to wrapped object)
266    * @throws IOException when file can not be read
267    * @throws JsonSyntaxException on bad file or when class has not been restored correctly
268    * @throws JsonIOException This exception is raised when Gson was unable to read an input stream
269    *         or write to on
270    */
271   public Serializer<T> fromReader(final Reader reader)
272           throws JsonSyntaxException, JsonIOException, Exception {
273 
274     // warn user if newer config is load to older QuimP
275     if (qconfVersionToLoad > convertStringVersion(timeStamp.getVersion())) {
276       LOGGER.info("You are trying to load config file which is in newer version"
277               + " than software you are using. (" + qconfVersionToLoad + " vs "
278               + convertStringVersion(timeStamp.getVersion()) + ")");
279     }
280 
281     // set version to load (read from file)
282     gsonBuilder.setVersion(qconfVersionToLoad);
283 
284     Gson gson = gsonBuilder.create();
285     Serializer<T> localref;
286     localref = gson.fromJson(reader, this);
287     verify(localref); // verification of correctness and conversion to current format
288 
289     if (doAfterSerialize) {
290       localref.obj.afterSerialize();
291     }
292     return localref;
293   }
294 
295   /**
296    * Perform basic verification of loaded file.
297    * 
298    * <p>It verifies rather on general level for fields added by Serializer itself. More detailed
299    * verification related to serialized class should be performed after full restoration of
300    * wrapped object.
301    * 
302    * @param localref object to verify
303    * @throws JsonSyntaxException on bad file or when class has not been restored correctly
304    */
305   private void verify(Serializer<T> localref) throws JsonSyntaxException {
306     // basic verification of loaded file, check whether some fields have reasonable values
307     try {
308       if (localref.obj == null || localref.className.isEmpty() || localref.createdOn.isEmpty()) {
309         throw new JsonSyntaxException("Can not map loaded gson to class. Is it proper file?");
310       }
311       convert(localref);
312     } catch (NullPointerException | IllegalArgumentException | QuimpException np) {
313       throw new JsonSyntaxException("Can not map loaded gson to class. Is it proper file?", np);
314     }
315   }
316 
317   /**
318    * This method is called on load and goes through registered converters executing them.
319    * 
320    * <p>Perform conversions from older version to current (newer).
321    * 
322    * @param localref restored object
323    * @throws QuimpException on problems with conversion
324    * @see #registerConverter(IQconfOlderConverter)
325    */
326   private void convert(Serializer<T> localref) throws QuimpException {
327     if (converters.isEmpty()) {
328       return; // no converters registered
329     }
330 
331     for (IQconfOlderConverter<T> converter : converters) {
332       // compare version loaded from file. If read version from file is smaller than returned
333       // by converter - execute conversion
334       if (converter.executeForLowerThan() > qconfVersionToLoad) {
335         converter.upgradeFromOld(localref);
336       }
337     }
338   }
339 
340   /**
341    * This method register format converter on list of converters.
342    * 
343    * <p>Registered converters are called on every object deserialisation in order that they were
344    * registered. Converter is run when version of tool is higher than version of converter.
345    * 
346    * @param converter converter
347    * @see IQconfOlderConverter
348    * @see #convert(Serializer)
349    */
350   public void registerConverter(IQconfOlderConverter<T> converter) {
351     converters.add(converter);
352   }
353 
354   /**
355    * Convert wrapped class to JSON representation together with Serializer wrapper
356    * 
357    * <p>Calls com.github.celldynamics.quimp.IQuimpSerialize.beforeSerialize() before conversion
358    * 
359    * @return JSON string
360    * @see com.github.celldynamics.quimp.Serializer#setPretty()
361    */
362   @Override
363   public String toString() {
364     // set version to save (read from calee)
365     gsonBuilder.setVersion(qconfVersionToSave);
366     Gson gson = gsonBuilder.create();
367     if (obj != null && doBeforeSerialize == true) {
368       obj.beforeSerialize();
369     }
370     return gson.toJson(this);
371   }
372 
373   /**
374    * Get current date included in saved file.
375    * 
376    * @return Formatted string with date.
377    */
378   public String getCurrentDate() {
379     Date dateNow = new Date();
380     SimpleDateFormat df = new SimpleDateFormat("E yyyy.MM.dd 'at' HH:mm:ss a zzz");
381     return df.format(dateNow);
382   }
383 
384   /**
385    * Dump object.
386    *
387    * @param obj object to dump (must be packed in Serializer already)
388    * @param filename filename
389    * @param savePretty true if use pretty format
390    * @throws FileNotFoundException on saving problem
391    * @see #jsonDump(Object, String, boolean)
392    * @deprecated It does not support GSon versioning
393    */
394   @Deprecated
395   static void jsonDump(final Object obj, final String filename, boolean savePretty)
396           throws FileNotFoundException {
397     File file = new File(filename);
398     Serializer.jsonDump(obj, file, savePretty);
399   }
400 
401   /**
402    * Performs pure dump of provided object without packing it into super class
403    * 
404    * <p><b>Warning</b>
405    * 
406    * <p>This method does not call beforeSerialize(). It must be called explicitly before dumping.
407    * Can be used for saving already packed objects
408    * 
409    * @param obj to dump
410    * @param filename to be saved under
411    * @param savePretty if \a true use pretty format
412    * @throws FileNotFoundException when file can not be created
413    * @deprecated It does not support GSon versioning
414    */
415   @Deprecated
416   static void jsonDump(final Object obj, final File filename, boolean savePretty)
417           throws FileNotFoundException {
418     GsonBuilder gsonBuilder = new GsonBuilder();
419     if (savePretty) {
420       gsonBuilder.setPrettyPrinting();
421     }
422     Gson gson = gsonBuilder.create();
423     if (obj != null) {
424       String str = gson.toJson(obj);
425       PrintWriter f;
426       f = new PrintWriter(filename);
427       f.print(str);
428       f.close();
429     }
430   }
431 
432   /**
433    * Sets pretty JSON formatting on save operation.
434    * 
435    * @see com.github.celldynamics.quimp.Serializer#toString()
436    * @see com.github.celldynamics.quimp.Serializer#save(String)
437    */
438   public void setPretty() {
439     gsonBuilder.setPrettyPrinting();
440   }
441 
442   /**
443    * Read QuimP version from QCONF file.
444    * 
445    * <p>It does not deserialize JSON, just plain string reading from file.
446    * 
447    * @param reader reader that delivers string
448    * @return Version string encoded as double. Any -SNAPSHOT suffix is removed. Return 0.0 on
449    *         error.
450    * @throws JsonSyntaxException on version read error
451    */
452   public Double getQconfVersion(Reader reader) {
453     // key to look for
454     final String versionKey = "\"version\"";
455     char[] buf = new char[256];
456     try {
457       reader.read(buf);
458     } catch (IOException e) {
459       throw new JsonSyntaxException("JSON string can not be read", e);
460     }
461     String sbuf = new String(buf);
462     LOGGER.trace("Header: " + sbuf);
463     int pos = sbuf.indexOf(versionKey);
464     if (pos < 0) {
465       throw new JsonSyntaxException("JSON file does not contain version tag");
466     }
467     pos = sbuf.indexOf("\"", pos + versionKey.length());
468     int pos2 = sbuf.indexOf("\"", pos + 1);
469     String version = sbuf.substring(pos + 1, pos2);
470     Double ret = convertStringVersion(version);
471     return ret;
472   }
473 
474   /**
475    * Convert string in format a.b.c-SNAPSHOT to double a.bc
476    * 
477    * @param ver String version to convert
478    * @return Double representation of version string
479    * @throws JsonSyntaxException on wrong conversions (due to e.g. wrong wile read or bad
480    *         structure)
481    */
482   private Double convertStringVersion(String ver) {
483     String ret;
484     try {
485       // remove "" and other stuff
486       ret = ver.replaceAll("([ \",]|-SNAPSHOT)", "");
487       int dotcount = ret.length() - ret.replace(".", "").length();
488       if (dotcount > 2) {
489         throw new JsonSyntaxException("Format of version string must follow rule major.minor.inc");
490       }
491       if (dotcount == 2) {
492         int seconddotpos = ret.lastIndexOf('.');
493         ret = ret.substring(0, seconddotpos) + ret.substring(seconddotpos + 1);
494       }
495       return new Double(ret);
496     } catch (NumberFormatException ex) {
497       throw new JsonSyntaxException("Version string could not be converted to number", ex);
498     }
499   }
500 
501   /**
502    * Register constructor for wrapped class.
503    * 
504    * <p>It may be necessary during loading JSON file if wrapped class needs some parameters to
505    * restore its state on com.github.celldynamics.quimp.IQuimpSerialize.afterSerialize() call and
506    * those
507    * parameters are passed in constructor.
508    * 
509    * @param type Type of class
510    * @param typeAdapter Wrapped object builder that implements InstanceCreator interface.
511    * @see com.github.celldynamics.quimp.filesystem.IQuimpSerialize#afterSerialize()
512    * @see <a href=
513    *      "GSon doc">https://github.com/google/gson/blob/master/UserGuide.md#TOC-InstanceCreator-for-a-Parameterized-Type</a>
514    */
515   public void registerInstanceCreator(Class<T> type, Object typeAdapter) {
516     gsonBuilder.registerTypeAdapter(type, typeAdapter);
517   }
518 
519   /*
520    * (non-Javadoc)
521    * 
522    * @see java.lang.reflect.ParameterizedType#getActualTypeArguments()
523    */
524   @Override
525   public Type[] getActualTypeArguments() {
526     return new Type[] { type };
527   }
528 
529   /*
530    * (non-Javadoc)
531    * 
532    * @see java.lang.reflect.ParameterizedType#getRawType()
533    */
534   @Override
535   public Type getRawType() {
536     return Serializer.class;
537   }
538 
539   /*
540    * (non-Javadoc)
541    * 
542    * @see java.lang.reflect.ParameterizedType#getOwnerType()
543    */
544   @Override
545   public Type getOwnerType() {
546     return null;
547   }
548 
549 }