Lagom specifics
GDPR for Akka Persistence can be used with Lagom PersistentEntity Lagom PersistentEntity, and provides encryption for WithDataSubjectId
when you are using Jackson or Play JSON for serialization.
Lagom PersistentEntity
The PersistentEntity
defines the type for events and therefore we recommend using the WithDataSubjectId
inside the events rather than wrapping the events with WithDataSubjectId
. To wrap the full events, the event type of the PersistentEntity
would have to be WithDataSubjectId<Object>
WithDataSubjectId[AnyRef]
and the benefits of the type information would not be available.
Using WithDataSubjectId
inside the events and snapshots requires no additional effort when the JSON serializer is used.
Enable the GdprModule
for Jackson serializion as described in Serialization with Jackson Play JSON format as described Serialization with Play JSON.
Here is an example of a PersistentEntity
that is using WithDataSubjectId
in an event and snapshot.
The event:
- Java
-
/** * An event that represents a change in greeting message. */ @SuppressWarnings("serial") @Value @JsonDeserialize final class GreetingMessageChanged implements HelloEvent { public final WithDataSubjectId<String> message; @JsonCreator GreetingMessageChanged(WithDataSubjectId<String> message) { this.message = Preconditions.checkNotNull(message, "message"); } }
The snapshot:
- Java
-
/** * The state for the {@link HelloEntity} entity. */ @SuppressWarnings("serial") @Value @JsonDeserialize public final class HelloState implements Jsonable { public final String timestamp; public final WithDataSubjectId<String> message; @JsonCreator public HelloState(String timestamp, WithDataSubjectId<String> message) { this.message = Preconditions.checkNotNull(message, "message"); this.timestamp = Preconditions.checkNotNull(timestamp, "timestamp"); } }
The entity:
- Java
-
/** * This is an event sourced entity. It has a state, {@link HelloState}, which * stores what the greeting should be (eg, "Hello Alice"). * <p> * Event sourced entities are interacted with by sending them commands. This * entity supports two commands, a {@link UseGreetingMessage} command, which is * used to change the greeting, and a {@link Hello} command, which is a read * only command which returns a greeting to the name specified by the command. * <p> * Commands get translated to events, and it's the events that get persisted by * the entity. Each event will have an event handler registered for it, and an * event handler simply applies an event to the current state. This will be done * when the event is first created, and it will also be done when the entity is * loaded from the database - each event will be replayed to recreate the state * of the entity. * <p> * This entity defines one event, the {@link GreetingMessageChanged} event, * which is emitted when a {@link UseGreetingMessage} command is received. */ public class HelloEntity extends PersistentEntity<HelloCommand, HelloEvent, HelloState> { /** * An entity can define different behaviours for different states, but it will * always start with an initial behaviour. This entity only has one behaviour. */ @Override public Behavior initialBehavior(Optional<HelloState> snapshotState) { /* * Behaviour is defined using a behaviour builder. The behaviour builder * starts with a state, if this entity supports snapshotting (an * optimisation that allows the state itself to be persisted to combine many * events into one), then the passed in snapshotState may have a value that * can be used. * * Otherwise, the default state is to use the Hello greeting. */ BehaviorBuilder b = newBehaviorBuilder( snapshotState.orElse(new HelloState(LocalDateTime.now().toString(), WithDataSubjectId.create(entityId(), "Hello"))) ); /* * Command handler for the UseGreetingMessage command. */ b.setCommandHandler(UseGreetingMessage.class, (cmd, ctx) -> // In response to this command, we want to first persist it as a // GreetingMessageChanged event ctx.thenPersist(new GreetingMessageChanged(WithDataSubjectId.create(entityId(), cmd.message)), // Then once the event is successfully persisted, we respond with done. evt -> ctx.reply(Done.getInstance()) ) ); /* * Event handler for the GreetingMessageChanged event. */ b.setEventHandler(GreetingMessageChanged.class, // We simply update the current state to use the greeting message from // the event. evt -> new HelloState(LocalDateTime.now().toString(), evt.message) ); /* * Command handler for the Hello command. */ b.setReadOnlyCommandHandler(Hello.class, (cmd, ctx) -> { // Get the greeting from the current state, and send a reply with that message. String greetingMessage; if (state().message.isShredded()) greetingMessage = "Hello"; else greetingMessage = state().message.getPayload().get(); ctx.reply(greetingMessage); } ); /* * We've defined all our behaviour, so build and return it. */ return b.build(); } }
Serialization with Jackson
When using Jackson for serialization of event and snapshots the akka-gdpr-jackson
module can be used to automatically encrypt WithDataSubjectId
both at the top level or as parts inside the data structure.
Add this dependency for Jackson serialization:
- sbt
libraryDependencies += "com.lightbend.akka" %% "akka-gdpr-jackson" % "1.1.16"
- Maven
<dependency> <groupId>com.lightbend.akka</groupId> <artifactId>akka-gdpr-jackson_2.11</artifactId> <version>1.1.16</version> </dependency>
- Gradle
dependencies { compile group: 'com.lightbend.akka', name: 'akka-gdpr-jackson_2.11', version: '1.1.16' }
Then register the Jackson module akka.persistence.gdpr.jackson.GdprModule
in the ObjectMapper
.
When using Lagom JSON Serialization with Jackson this is done with configuration:
lagom.serialization.json.jackson-modules += "akka.persistence.gdpr.jackson.GdprModule"
Register the Jackson serializer for the WithDataSubjectId
class. Add the following configuration when using Lagom:
akka.actor {
serialization-bindings {
"akka.persistence.gdpr.WithDataSubjectId" = lagom-json
}
}
The WithDataSubjectId
is serialized as JSON:
{
"dataSubjectId": "bobId",
"payload": "CgVib2JJZBKRAXQVOzc8ZaMy3Z/N9VUfb1/uoIPEAi7R+fLwgkJhQxbBfqKRxFuYpm7dpjip5tDpSiN2UQIue+71i70m63E4RQb6vq6IUzx3nX57GkKe01qMd3R4/sTUNwjIldC+bob9TH+DBAI5QG9K4oBEiA3iWdcYTFCNhS8BBg6hUwxtYwbmWi5q9c/TtvmWLznlCNFC9e0YGyIQamF2YS5sYW5nLlN0cmluZw=="
}
Where the payload
is Jackson’s binary encoding (Base64) of the Protobuf representation of the WithDataSubjectId
as described in How the module works, including encryption of the actual payload data.
Serialization with Play JSON
When using Lagom JSON Serialization with Play JSON for serialization of event and snapshots the akka-gdpr-playjson
module can be used to automatically encrypt WithDataSubjectId
both at the top level or as parts inside the data structure.
Add this dependency for Play JSON serialization:
- sbt
libraryDependencies += "com.lightbend.akka" %% "akka-gdpr-playjson" % "1.1.16"
- Maven
<dependency> <groupId>com.lightbend.akka</groupId> <artifactId>akka-gdpr-playjson_2.11</artifactId> <version>1.1.16</version> </dependency>
- Gradle
dependencies { compile group: 'com.lightbend.akka', name: 'akka-gdpr-playjson_2.11', version: '1.1.16' }
Register the Format
for WithDataSubjectId
class in Lagom’s JsonSerializerRegistry
as described in the Lagom documentation.
The Format
for WithDataSubjectId
is provided by akka-gdpr-playjson
in GdprFormat.withDataSubjectIdFormat
and can be defined in a JsonSerializerRegistry
like this:
- Scala
-
import akka.persistence.gdpr.playjson.GdprFormat import akka.persistence.gdpr.WithDataSubjectId import com.lightbend.lagom.scaladsl.playjson.JsonSerializer import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry import scala.collection.immutable object ExampleEntity { final case class PersonalInformation(name: String, email: String) object PersonalInformation { implicit val format: Format[PersonalInformation] = Json.format[PersonalInformation] } trait Event final case class Event1(field1: String) extends Event object Event1 { implicit val format: Format[Event1] = Json.format[Event1] } final case class UserRegistered(personal: WithDataSubjectId[PersonalInformation]) extends Event object UserRegistered { implicit val inner: Format[WithDataSubjectId[PersonalInformation]] = GdprFormat.specificWithDataSubjectIdFormat[PersonalInformation] implicit val format: Format[UserRegistered] = Json.format[UserRegistered] } final case class State( part1: WithDataSubjectId[String], part2: WithDataSubjectId[PersonalInformation], other: String) object State { implicit val inner1: Format[WithDataSubjectId[String]] = GdprFormat.specificWithDataSubjectIdFormat[String] implicit val inner2: Format[WithDataSubjectId[PersonalInformation]] = GdprFormat.specificWithDataSubjectIdFormat[PersonalInformation] implicit val format: Format[State] = Json.format[State] } } object ExampleRegistry extends JsonSerializerRegistry { import ExampleEntity._ import GdprFormat.withDataSubjectIdFormat override def serializers: immutable.Seq[JsonSerializer[_]] = List( JsonSerializer[Event1], JsonSerializer[State], JsonSerializer[UserRegistered], JsonSerializer[PersonalInformation], JsonSerializer[WithDataSubjectId[_]]) }
Note how the import GdprFormat.withDataSubjectIdFormat
adds the format for WithDataSubjectId[_]
in implicit scope and can be used for top level WithDataSubjectId
. For nested WithDataSubjectId
you must define a more specifific type with GdprFormat.specificWithDataSubjectIdFormat
. For example specificWithDataSubjectIdFormat[String]
and specificWithDataSubjectIdFormat[PersonalInformation]
are used in the above example.
The WithDataSubjectId
is serialized as JSON:
{
"dataSubjectId": "bobId",
"payload": "CgVib2JJZBKRAXQVOzc8ZaMy3Z/N9VUfb1/uoIPEAi7R+fLwgkJhQxbBfqKRxFuYpm7dpjip5tDpSiN2UQIue+71i70m63E4RQb6vq6IUzx3nX57GkKe01qMd3R4/sTUNwjIldC+bob9TH+DBAI5QG9K4oBEiA3iWdcYTFCNhS8BBg6hUwxtYwbmWi5q9c/TtvmWLznlCNFC9e0YGyIQamF2YS5sYW5nLlN0cmluZw=="
}
Where the payload
is Jackson’s binary encoding (Base64) of the Protobuf representation of the WithDataSubjectId
as described in How the module works, including encryption of the actual payload data.