com.clouway.kcqrs.client.HttpEventStore.kt Maven / Gradle / Ivy
The newest version!
package com.clouway.kcqrs.client
import com.clouway.kcqrs.core.Aggregate
import com.clouway.kcqrs.core.Binary
import com.clouway.kcqrs.core.EventPayload
import com.clouway.kcqrs.core.EventStore
import com.clouway.kcqrs.core.GetAllEventsRequest
import com.clouway.kcqrs.core.GetAllEventsResponse
import com.clouway.kcqrs.core.GetEventsFromStreamsRequest
import com.clouway.kcqrs.core.GetEventsResponse
import com.clouway.kcqrs.core.IndexedEvent
import com.clouway.kcqrs.core.Position
import com.clouway.kcqrs.core.RevertEventsResponse
import com.clouway.kcqrs.core.SaveEventsRequest
import com.clouway.kcqrs.core.SaveEventsResponse
import com.clouway.kcqrs.core.SaveOptions
import com.clouway.kcqrs.core.Snapshot
import com.google.api.client.http.GenericUrl
import com.google.api.client.http.HttpRequestFactory
import com.google.api.client.http.HttpStatusCodes
import com.google.api.client.http.json.JsonHttpContent
import com.google.api.client.json.GenericJson
import com.google.api.client.json.gson.GsonFactory
import com.google.api.client.util.Key
import java.io.IOException
import java.net.URL
import java.net.URLEncoder
import java.util.Arrays
/**
* HttpEventStore is an implementation of EventStore which uses REST api for storing and retrieving of events.
*
* @author Miroslav Genov ([email protected])
*/
class HttpEventStore(private val endpoint: URL,
private val requestFactory: HttpRequestFactory,
private val timeout: Int = 60000) : EventStore {
override fun saveEvents(request: SaveEventsRequest, saveOptions: SaveOptions): SaveEventsResponse {
val requestEvents = request.events.map {
EventPayloadDto(it.aggregateId, it.kind, it.timestamp, it.identityId, it.data.payload.toString(Charsets.UTF_8))
}
try {
var snapshotDto: SnapshotDto? = null
if (saveOptions.createSnapshot.required && saveOptions.createSnapshot.snapshot != null) {
snapshotDto = SnapshotDto(saveOptions.createSnapshot.snapshot!!.version, BinaryDto(saveOptions.createSnapshot.snapshot!!.data.payload))
}
val httpRequest = requestFactory.buildPostRequest(
GenericUrl("$endpoint/v1/aggregates"),
JsonHttpContent(
GsonFactory.getDefaultInstance(),
SaveEventsRequestDto(
request.tenant,
request.stream,
request.aggregateType,
saveOptions.version,
requestEvents,
saveOptions.topicName,
saveOptions.createSnapshot.required,
snapshotDto
)
)
).setConnectTimeout(timeout).setReadTimeout(timeout)
httpRequest.headers.set("Accept-version", "v2")
httpRequest.throwExceptionOnExecuteError = false
val response = httpRequest.execute()
if (response.isSuccessStatusCode) {
val resp = response.parseAs(SaveEventsResponseDto::class.java)
return SaveEventsResponse.Success(resp.version, resp.sequenceIds, adapt(resp.aggregate))
}
if (response.statusCode == HttpStatusCodes.STATUS_CODE_CONFLICT) {
val resp = response.parseAs(SaveEventsResponseDto::class.java)
return SaveEventsResponse.EventCollision(resp.version)
}
if (response.statusCode == HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY) {
val resp = response.parseAs(SnapshotRequiredDto::class.java)
var snapshot: Snapshot? = null
if (resp.currentSnapshot != null) {
snapshot = Snapshot(resp.currentSnapshot!!.version, Binary(resp.currentSnapshot!!.data!!.payload))
}
return SaveEventsResponse.SnapshotRequired(
resp.currentEvents.map { EventPayload(it.aggregateId, it.kind, it.timestamp, it.identityId, Binary(it.payload)) },
snapshot,
resp.version
)
}
if (response.statusCode == HttpStatusCodes.STATUS_CODE_BAD_GATEWAY) {
return SaveEventsResponse.Error("Unable to publish event")
}
} catch (ex: IOException) {
return SaveEventsResponse.ErrorInCommunication(ex.message!!)
}
return SaveEventsResponse.Error("Generic Error")
}
override fun getEventsFromStreams(request: GetEventsFromStreamsRequest): GetEventsResponse {
val tenant = request.tenant
val streams = request.streams
val streamIds = streams.joinToString(",")
val params = "tenant=$tenant&streams=$streamIds"
val httpRequest = requestFactory.buildGetRequest(GenericUrl("$endpoint/v2/aggregates?ids=$streamIds&$params"))
.setConnectTimeout(timeout).setReadTimeout(timeout)
httpRequest.headers.set("Accept-version", "v2")
httpRequest.throwExceptionOnExecuteError = false
try {
val response = httpRequest.execute()
// Aggregate was not found and no events cannot be returned
if (response.statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
return GetEventsResponse.AggregateNotFound(streams, streams.get(0))
}
if (response.isSuccessStatusCode) {
val resp = response.parseAs(GetEventsResponseDto::class.java)
val aggregates = resp.aggregates.map {
adapt(it)
}
return GetEventsResponse.Success(aggregates)
}
return GetEventsResponse.Error("got unknown error")
} catch (ex: IOException) {
return GetEventsResponse.ErrorInCommunication(ex.message!!)
}
}
override fun getAllEvents(request: GetAllEventsRequest): GetAllEventsResponse {
val all = URLEncoder.encode("\$all", "UTF-8")
val streams = request.streams.joinToString(separator = ",")
val streamsParam = if (streams.isNotEmpty()) "&streams=$streams" else ""
val url = endpoint.toString() + "/v2/aggregates/$all?fromPosition=${
request.position?.value
?: 0
}&maxCount=${request.maxCount}&readDirection=${request.readDirection.name}$streamsParam"
val req = requestFactory.buildGetRequest(GenericUrl(url))
.setConnectTimeout(timeout).setReadTimeout(timeout)
req.headers.set("Accept-version", "v2")
req.throwExceptionOnExecuteError = false
try {
val response = req.execute()
// Aggregate was not found and no events cannot be returned
if (response.statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
return GetAllEventsResponse.Success(listOf(), request.readDirection, null)
}
if (response.isSuccessStatusCode) {
val resp = response.parseAs(GetAllEventsResponseDto::class.java)
val events = resp.events.map {
IndexedEvent(
Position(it.position),
it.tenant,
it.aggregateType,
it.version,
EventPayload(it.payload.aggregateId, it.payload.kind, it.payload.timestamp, it.payload.identityId, Binary(it.payload.payload))
)
}
return GetAllEventsResponse.Success(events, request.readDirection, Position(resp.nextPosition ?: 0))
}
return GetAllEventsResponse.Error("got unknown error")
} catch (ex: IOException) {
return GetAllEventsResponse.ErrorInCommunication(ex.message!!)
}
}
override fun revertLastEvents(tenant: String, stream: String, count: Int): RevertEventsResponse {
val request = requestFactory.buildPatchRequest(
GenericUrl("$endpoint/v1/aggregates/$stream?&aggregateType=$tenant"),
JsonHttpContent(GsonFactory.getDefaultInstance(), RevertEventsRequestDto(stream, count))
).setConnectTimeout(timeout).setReadTimeout(timeout)
request.headers.set("Accept-version", "v2")
request.throwExceptionOnExecuteError = false
try {
val response = request.execute()
// Aggregate was not found, so no events cannot be returned
if (response.statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
return RevertEventsResponse.AggregateNotFound(stream, tenant)
}
if (response.isSuccessStatusCode) {
return RevertEventsResponse.Success(listOf())
}
return RevertEventsResponse.Error("Generic Error")
} catch (ex: IOException) {
return RevertEventsResponse.Error("Communication Error")
}
}
}
private fun adapt(it: AggregateDto): Aggregate {
var snapshot: Snapshot? = null
if (it.snapshot != null) {
snapshot = Snapshot(it.snapshot!!.version, Binary(it.snapshot!!.data!!.payload))
}
return Aggregate(
it.aggregateType,
snapshot,
it.version,
it.events.map { event ->
EventPayload(
event.aggregateId,
event.kind,
event.timestamp,
event.identityId,
Binary(event.payload)
)
}
)
}
internal data class GetAllEventsResponseDto(
@Key @JvmField var events: List,
@Key @JvmField var readDirection: String?,
@Key @JvmField var nextPosition: Long?
) : GenericJson() {
@Suppress("UNUSED")
constructor() : this(mutableListOf(), null, 0L)
}
internal data class IndexedEventDto(
@Key @JvmField var position: Long,
@Key @JvmField var tenant: String,
@Key @JvmField var aggregateType: String,
@Key @JvmField var version: Long,
@Key @JvmField var payload: EventPayloadDto
) : GenericJson() {
@Suppress("UNUSED")
constructor() : this(0L, "", "", 0L, EventPayloadDto("", "", 0L, "", ""))
}
internal data class GetEventsResponseDto(@Key @JvmField var aggregates: List) : GenericJson() {
@Suppress("UNUSED")
constructor() : this(mutableListOf())
}
internal data class AggregateDto(@Key @JvmField var aggregateType: String, @Key @JvmField var snapshot: SnapshotDto?, @Key @JvmField var version: Long, @Key @JvmField var topic: String, @Key @JvmField var events: List) {
constructor() : this("", null, 0L, "", listOf())
}
internal data class EventPayloadDto(@Key @JvmField var aggregateId: String, @Key @JvmField var kind: String, @Key @JvmField var timestamp: Long, @Key @JvmField var identityId: String, @Key @JvmField var payload: String) : GenericJson() {
@Suppress("UNUSED")
constructor() : this("", "", 0L, "", "")
}
internal data class SaveEventsRequestDto(@Key @JvmField var tenant: String,
@Key @JvmField var stream: String,
@Key @JvmField var aggregateType: String,
@Key @JvmField var version: Long,
@Key @JvmField var events: List,
@Key @JvmField var topicName: String,
@Key @JvmField var snapshotRequired: Boolean,
@Key @JvmField var snapshot: SnapshotDto?
) : GenericJson() {
@Suppress("UNUSED")
constructor() : this("", "", "", 0L, mutableListOf(), "", false, null)
}
internal data class SaveEventsResponseDto(
@Key @JvmField var aggregateId: String,
@Key @JvmField var version: Long,
@Key @JvmField var sequenceIds: List,
@Key @JvmField var aggregate: AggregateDto
) : GenericJson() {
@Suppress("UNUSED")
constructor() : this("", 0L, listOf(), AggregateDto())
}
internal data class RevertEventsRequestDto(@Key @JvmField var aggregateId: String, @Key @JvmField var count: Int) : GenericJson() {
@Suppress("UNUSED")
constructor() : this("", 0)
}
internal data class SnapshotRequiredDto(
@Key @JvmField var currentEvents: List,
@Key @JvmField var currentSnapshot: SnapshotDto? = null,
@Key @JvmField var version: Long) : GenericJson() {
@Suppress("UNUSED")
constructor() : this(listOf(), null, 0L)
}
internal data class SnapshotDto(@Key @JvmField var version: Long, @Key @JvmField var data: BinaryDto?) : GenericJson() {
@Suppress("UNUSED")
constructor() : this(0, null)
}
internal data class BinaryDto(@Key @JvmField var payload: ByteArray) : GenericJson() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as BinaryDto
if (!Arrays.equals(payload, other.payload)) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + Arrays.hashCode(payload)
return result
}
@Suppress("UNUSED")
constructor() : this(ByteArray(0))
}