fi.luontola.cqrshotel.framework.PsqlEventStore.java Source code

Java tutorial

Introduction

Here is the source code for fi.luontola.cqrshotel.framework.PsqlEventStore.java

Source

// Copyright  2016-2017 Esko Luontola
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

package fi.luontola.cqrshotel.framework;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.postgresql.util.PSQLException;
import org.postgresql.util.ServerErrorMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Array;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PsqlEventStore implements EventStore {

    private static final Logger log = LoggerFactory.getLogger(PsqlEventStore.class);

    private static final Pattern OPTIMISTIC_LOCKING_FAILURE_MESSAGE = Pattern
            .compile("^optimistic locking failure, current version is (\\d+)$");

    private final DataSource dataSource;
    private final NamedParameterJdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;

    public PsqlEventStore(DataSource dataSource, ObjectMapper objectMapper) {
        this.dataSource = dataSource;
        this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
        this.objectMapper = objectMapper;
    }

    @Override
    public long saveEvents(UUID streamId, List<Event> newEvents, int expectedVersion) {
        try (Connection connection = DataSourceUtils.getConnection(dataSource)) {
            Array data = connection.createArrayOf("jsonb", serializeData(newEvents));
            Array metadata = connection.createArrayOf("jsonb", serializeMetadata(newEvents));

            long endPosition = jdbcTemplate.queryForObject(
                    "SELECT save_events(:stream_id, :expected_version, :data, :metadata)",
                    new MapSqlParameterSource().addValue("stream_id", streamId)
                            .addValue("expected_version", expectedVersion).addValue("data", data)
                            .addValue("metadata", metadata),
                    Long.class);

            if (log.isTraceEnabled()) {
                for (int i = 0; i < newEvents.size(); i++) {
                    int newVersion = expectedVersion + 1 + i;
                    Event newEvent = newEvents.get(i);
                    log.trace("Saved stream {} version {}: {}", streamId, newVersion, newEvent);
                }
            }
            return endPosition;

        } catch (UncategorizedSQLException e) {
            if (e.getCause() instanceof PSQLException) {
                ServerErrorMessage serverError = ((PSQLException) e.getCause()).getServerErrorMessage();
                Matcher m = OPTIMISTIC_LOCKING_FAILURE_MESSAGE.matcher(serverError.getMessage());
                if (m.matches()) {
                    String currentVersion = m.group(1);
                    throw new OptimisticLockingException("expected version " + expectedVersion + " but was "
                            + currentVersion + " for stream " + streamId, e);
                }
            }
            throw e;
        } catch (SQLException | JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<Event> getEventsForStream(UUID streamId, int sinceVersion) {
        return jdbcTemplate.query(
                "SELECT data, metadata " + "FROM event " + "WHERE stream_id = :stream_id "
                        + "  AND version > :since_version " + "ORDER BY version",
                new MapSqlParameterSource().addValue("stream_id", streamId).addValue("since_version", sinceVersion),
                this::eventMapping);
    }

    @Override
    public List<Event> getAllEvents(long sincePosition) {
        return jdbcTemplate.query(
                "SELECT e.data, e.metadata " + "FROM event e " + "JOIN event_sequence s USING (stream_id, version) "
                        + "WHERE s.position > :position " + "ORDER BY s.position",
                new MapSqlParameterSource("position", sincePosition), this::eventMapping);
    }

    @Override
    public int getCurrentVersion(UUID streamId) {
        List<Integer> version = jdbcTemplate.queryForList("SELECT version FROM stream WHERE stream_id = :stream_id",
                new MapSqlParameterSource("stream_id", streamId), Integer.class);
        return version.isEmpty() ? BEGINNING : version.get(0);
    }

    @Override
    public long getCurrentPosition() {
        List<Long> position = jdbcTemplate.queryForList(
                "SELECT position FROM event_sequence ORDER BY position DESC  LIMIT 1", new MapSqlParameterSource(),
                Long.class);
        return position.isEmpty() ? BEGINNING : position.get(0);
    }

    private Event eventMapping(ResultSet rs, int rowNum) throws SQLException {
        String data = rs.getString("data");
        String metadata = rs.getString("metadata");
        return deserialize(data, metadata);
    }

    private String[] serializeData(List<Event> events) throws JsonProcessingException {
        return events.stream().map(this::serialize).toArray(String[]::new);
    }

    private String[] serializeMetadata(List<Event> events) throws JsonProcessingException {
        return events.stream().map(PsqlEventStore::getMetadata).map(this::serialize).toArray(String[]::new);
    }

    private static EventMetadata getMetadata(Event event) {
        EventMetadata meta = new EventMetadata();
        meta.type = event.getClass().getName();
        return meta;
    }

    private String serialize(Object event) {
        try {
            return objectMapper.writeValueAsString(event);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to serialize: " + event, e);
        }
    }

    private Event deserialize(String dataJson, String metadataJson) {
        try {
            EventMetadata metadata = objectMapper.readValue(metadataJson, EventMetadata.class);
            return (Event) objectMapper.readValue(dataJson, Class.forName(metadata.type));
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}