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.