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 }