/** * Copyright (C) 2009-2015 Typesafe Inc. */ package docs.persistence; import docs.persistence.proto.FlightAppModels; import java.nio.charset.Charset; import spray.json.JsObject; import akka.japi.Util; import akka.persistence.journal.EventAdapter; import akka.persistence.journal.EventSeq; import akka.protobuf.InvalidProtocolBufferException; import akka.serialization.SerializerWithStringManifest; public class PersistenceSchemaEvolutionDocTest { static //#protobuf-read-optional-model public enum SeatType { Window("W"), Aisle("A"), Other("O"), Unknown(""); private final String code; private SeatType(String code) { this.code = code; } public static SeatType fromCode(String c) { if (Window.code.equals(c)) return Window; else if (Aisle.code.equals(c)) return Aisle; else if (Other.code.equals(c)) return Other; else return Unknown; } } //#protobuf-read-optional-model static //#protobuf-read-optional-model public class SeatReserved { public final String letter; public final int row; public final SeatType seatType; public SeatReserved(String letter, int row, SeatType seatType) { this.letter = letter; this.row = row; this.seatType = seatType; } } //#protobuf-read-optional-model static //#protobuf-read-optional /** * Example serializer impl which uses protocol buffers generated classes (proto.*) * to perform the to/from binary marshalling. */ public class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest { @Override public int identifier() { return 67876; } private final String seatReservedManifest = SeatReserved.class.getName(); @Override public String manifest(Object o){ return o.getClass().getName(); } @Override public Object fromBinary(byte[] bytes, String manifest) { if (seatReservedManifest.equals(manifest)) { // use generated protobuf serializer try { return seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes)); } catch (InvalidProtocolBufferException e) { throw new IllegalArgumentException(e.getMessage()); } } else { throw new IllegalArgumentException("Unable to handle manifest: " + manifest); } } @Override public byte[] toBinary(Object o) { if (o instanceof SeatReserved) { SeatReserved s = (SeatReserved) o; return FlightAppModels.SeatReserved.newBuilder() .setRow(s.row) .setLetter(s.letter) .setSeatType(s.seatType.code) .build().toByteArray(); } else { throw new IllegalArgumentException("Unable to handle: " + o); } } // -- fromBinary helpers -- private SeatReserved seatReserved(FlightAppModels.SeatReserved p) { return new SeatReserved(p.getLetter(), p.getRow(), seatType(p)); } // handle missing field by assigning "Unknown" value private SeatType seatType(FlightAppModels.SeatReserved p) { if (p.hasSeatType()) return SeatType.fromCode(p.getSeatType()); else return SeatType.Unknown; } } //#protobuf-read-optional public static class RenamePlainJson { static //#rename-plain-json public class JsonRenamedFieldAdapter implements EventAdapter { // use your favorite json library private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller(); private final String V1 = "v1"; private final String V2 = "v2"; // this could be done independently for each event type @Override public String manifest(Object event) { return V2; } @Override public JsObject toJournal(Object event) { return marshaller.toJson(event); } @Override public EventSeq fromJournal(Object event, String manifest) { if (event instanceof JsObject) { JsObject json = (JsObject) event; if (V1.equals(manifest)) json = rename(json, "code", "seatNr"); return EventSeq.single(json); } else { throw new IllegalArgumentException("Can only work with JSON, was: " + event.getClass().getName()); } } private JsObject rename(JsObject json, String from, String to) { // use your favorite json library to rename the field JsObject renamed = json; return renamed; } } //#rename-plain-json } public static class SimplestCustomSerializer { static //#simplest-custom-serializer-model public class Person { public final String name; public final String surname; public Person(String name, String surname) { this.name = name; this.surname = surname; } } //#simplest-custom-serializer-model static //#simplest-custom-serializer /** * Simplest possible serializer, uses a string representation of the Person class. * * Usually a serializer like this would use a library like: * protobuf, kryo, avro, cap'n proto, flatbuffers, SBE or some other dedicated serializer backend * to perform the actual to/from bytes marshalling. */ public class SimplestPossiblePersonSerializer extends SerializerWithStringManifest { private final Charset utf8 = Charset.forName("UTF-8"); private final String personManifest = Person.class.getName(); // unique identifier of the serializer @Override public int identifier() { return 1234567; } // extract manifest to be stored together with serialized object @Override public String manifest(Object o) { return o.getClass().getName(); } // serialize the object @Override public byte[] toBinary(Object obj) { if (obj instanceof Person) { Person p = (Person) obj; return (p.name + "|" + p.surname).getBytes(utf8); } else { throw new IllegalArgumentException( "Unable to serialize to bytes, clazz was: " + obj.getClass().getName()); } } // deserialize the object, using the manifest to indicate which logic to apply @Override public Object fromBinary(byte[] bytes, String manifest) { if (personManifest.equals(manifest)) { String nameAndSurname = new String(bytes, utf8); String[] parts = nameAndSurname.split("[|]"); return new Person(parts[0], parts[1]); } else { throw new IllegalArgumentException( "Unable to deserialize from bytes, manifest was: " + manifest + "! Bytes length: " + bytes.length); } } } //#simplest-custom-serializer } public static class SamplePayload { private final Object payload; public SamplePayload(Object payload) { this.payload = payload; } public Object getPayload() { return payload; } } //#split-events-during-recovery interface V1 {}; interface V2 {} //#split-events-during-recovery static //#split-events-during-recovery // V1 event: public class UserDetailsChanged implements V1 { public final String name; public final String address; public UserDetailsChanged(String name, String address) { this.name = name; this.address = address; } } //#split-events-during-recovery static //#split-events-during-recovery // corresponding V2 events: public class UserNameChanged implements V2 { public final String name; public UserNameChanged(String name) { this.name = name; } } //#split-events-during-recovery static //#split-events-during-recovery public class UserAddressChanged implements V2 { public final String address; public UserAddressChanged(String address) { this.address = address; } } //#split-events-during-recovery static //#split-events-during-recovery // event splitting adapter: public class UserEventsAdapter implements EventAdapter { @Override public String manifest(Object event) { return ""; } @Override public EventSeq fromJournal(Object event, String manifest) { if (event instanceof UserDetailsChanged) { UserDetailsChanged c = (UserDetailsChanged) event; if (c.name == null) return EventSeq.single(new UserAddressChanged(c.address)); else if (c.address == null) return EventSeq.single(new UserNameChanged(c.name)); else return EventSeq.create( new UserNameChanged(c.name), new UserAddressChanged(c.address)); } else { return EventSeq.single(event); } } @Override public Object toJournal(Object event) { return event; } } //#split-events-during-recovery static public class CustomerBlinked { public final long customerId; public CustomerBlinked(long customerId) { this.customerId = customerId; } } static //#string-serializer-skip-deleved-event-by-manifest public class EventDeserializationSkipped { public static EventDeserializationSkipped instance = new EventDeserializationSkipped(); private EventDeserializationSkipped() { } } //#string-serializer-skip-deleved-event-by-manifest static //#string-serializer-skip-deleved-event-by-manifest public class RemovedEventsAwareSerializer extends SerializerWithStringManifest { private final Charset utf8 = Charset.forName("UTF-8"); private final String customerBlinkedManifest = "blinked"; // unique identifier of the serializer @Override public int identifier() { return 8337; } // extract manifest to be stored together with serialized object @Override public String manifest(Object o) { if (o instanceof CustomerBlinked) return customerBlinkedManifest; else return o.getClass().getName(); } @Override public byte[] toBinary(Object o) { return o.toString().getBytes(utf8); // example serialization } @Override public Object fromBinary(byte[] bytes, String manifest) { if (customerBlinkedManifest.equals(manifest)) return EventDeserializationSkipped.instance; else return new String(bytes, utf8); } } //#string-serializer-skip-deleved-event-by-manifest static //#string-serializer-skip-deleved-event-by-manifest-adapter public class SkippedEventsAwareAdapter implements EventAdapter { @Override public String manifest(Object event) { return ""; } @Override public Object toJournal(Object event) { return event; } @Override public EventSeq fromJournal(Object event, String manifest) { if (event == EventDeserializationSkipped.instance) return EventSeq.empty(); else return EventSeq.single(event); } } //#string-serializer-skip-deleved-event-by-manifest-adapter //#string-serializer-handle-rename static //#string-serializer-handle-rename public class RenamedEventAwareSerializer extends SerializerWithStringManifest { private final Charset utf8 = Charset.forName("UTF-8"); // unique identifier of the serializer @Override public int identifier() { return 8337; } private final String oldPayloadClassName = "docs.persistence.OldPayload"; // class NOT available anymore private final String myPayloadClassName = SamplePayload.class.getName(); // extract manifest to be stored together with serialized object @Override public String manifest(Object o) { return o.getClass().getName(); } @Override public byte[] toBinary(Object o) { if (o instanceof SamplePayload) { SamplePayload s = (SamplePayload) o; return s.payload.toString().getBytes(utf8); } else { // previously also handled "old" events here. throw new IllegalArgumentException( "Unable to serialize to bytes, clazz was: " + o.getClass().getName()); } } @Override public Object fromBinary(byte[] bytes, String manifest) { if (oldPayloadClassName.equals(manifest)) return new SamplePayload(new String(bytes, utf8)); else if (myPayloadClassName.equals(manifest)) return new SamplePayload(new String(bytes, utf8)); else throw new IllegalArgumentException("unexpected manifest [" + manifest + "]"); } } //#string-serializer-handle-rename static //#detach-models // Domain model - highly optimised for domain language and maybe "fluent" usage public class Customer { public final String name; public Customer(String name) { this.name = name; } } //#detach-models static //#detach-models public class Seat { public final String code; public Seat(String code) { this.code = code; } public SeatBooked bookFor(Customer customer) { return new SeatBooked(code, customer); } } //#detach-models static //#detach-models public class SeatBooked { public final String code; public final Customer customer; public SeatBooked(String code, Customer customer) { this.code = code; this.customer = customer; } } //#detach-models static //#detach-models // Data model - highly optimised for schema evolution and persistence public class SeatBookedData { public final String code; public final String customerName; public SeatBookedData(String code, String customerName) { this.code = code; this.customerName = customerName; } } //#detach-models //#detach-models-adapter class DetachedModelsAdapter implements EventAdapter { @Override public String manifest(Object event) { return ""; } @Override public Object toJournal(Object event) { if (event instanceof SeatBooked) { SeatBooked s = (SeatBooked) event; return new SeatBookedData(s.code, s.customer.name); } else { throw new IllegalArgumentException("Unsupported: " + event.getClass()); } } @Override public EventSeq fromJournal(Object event, String manifest) { if (event instanceof SeatBookedData) { SeatBookedData d = (SeatBookedData) event; return EventSeq.single(new SeatBooked(d.code, new Customer(d.customerName))); } else { throw new IllegalArgumentException("Unsupported: " + event.getClass()); } } } //#detach-models-adapter static //#detach-models-adapter-json public class JsonDataModelAdapter implements EventAdapter { // use your favorite json library private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller(); @Override public String manifest(Object event) { return ""; } @Override public JsObject toJournal(Object event) { return marshaller.toJson(event); } @Override public EventSeq fromJournal(Object event, String manifest) { if (event instanceof JsObject) { JsObject json = (JsObject) event; return EventSeq.single(marshaller.fromJson(json)); } else { throw new IllegalArgumentException( "Unable to fromJournal a non-JSON object! Was: " + event.getClass()); } } } //#detach-models-adapter-json }