/*
* Copyright (C) 2014-2015 ULYSSIS VZW
*
* This file is part of i++.
*
* i++ is free software: you can redistribute it and/or modify
* it under the terms of version 3 of the GNU Affero General Public License
* as published by the Free Software Foundation. No other versions apply.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package org.ulyssis.ipp.snapshot;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ulyssis.ipp.utils.Serialization;
import java.io.IOException;
import java.sql.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
@JsonSubTypes({ @JsonSubTypes.Type(value=StartEvent.class),
@JsonSubTypes.Type(value=EndEvent.class),
@JsonSubTypes.Type(value=AddTagEvent.class),
@JsonSubTypes.Type(value=RemoveTagEvent.class),
@JsonSubTypes.Type(value=CorrectionEvent.class),
@JsonSubTypes.Type(value=TagSeenEvent.class),
@JsonSubTypes.Type(value=IdentityEvent.class),
@JsonSubTypes.Type(value=MessageEvent.class),
@JsonSubTypes.Type(value=StatusChangeEvent.class)})
public abstract class Event {
private static final Logger LOG = LogManager.getLogger(Event.class);
@JsonIgnore
private long id = -1;
@JsonIgnore
private boolean removed = false;
private Instant time;
protected Event(Instant time) {
this.time = time;
}
@JsonIgnore
public final Optional<Long> getId() {
if (id != -1) return Optional.of(id);
else return Optional.empty();
}
@JsonIgnore
public final boolean isRemoved() {
if (id == -1) throw new IllegalStateException("Trying to request removed state of event not in db");
return removed;
}
public final Instant getTime() {
return time;
}
/**
* Determines whether this event should be isUnique, defaults to false
*
* @return whether this event should be isUnique (default implementation = false)
*/
@JsonIgnore
public boolean isUnique() {
return false;
}
/**
* Determines whether this event can be removed or undone.
*
* @return whether this event can be removed or undone (default implementation = isUnique())
*/
@JsonIgnore
public boolean isRemovable() {
return isUnique();
}
/**
* Apply this event to a snapshot, yielding the new snapshot
*/
protected abstract Snapshot doApply(Snapshot before);
public final Snapshot apply(Snapshot before) {
assert !removed;
Snapshot result = doApply(before);
if (result == before && before.getEventId().isPresent()) {
// We need to copy it anyway
result = Snapshot.builder(getTime(), before).build();
}
result.eventId = this.id;
return result;
}
// TODO: How to deal with deserialization problem?
public static List<Event> loadAll(Connection connection) throws SQLException, IOException {
String statement = "SELECT \"id\", \"data\", \"removed\" FROM \"events\" ORDER BY \"time\" ASC, \"id\" ASC";
List<Event> events = new ArrayList<>();
try (Statement stmt = connection.createStatement();
ResultSet result = stmt.executeQuery(statement)) {
while (result.next()) {
String evString = result.getString("data");
Event event = Serialization.getJsonMapper().readValue(evString, Event.class);
event.id = result.getLong("id");
event.removed = result.getBoolean("removed");
events.add(event);
}
}
return events;
}
public static Optional<Event> loadUnique(Connection connection, Class<? extends Event> eventType) throws SQLException, IOException {
String statement = "SELECT \"id\", \"data\" FROM \"events\" WHERE \"type\" = ? AND \"removed\" = false";
try (PreparedStatement stmt = connection.prepareStatement(statement)) {
stmt.setString(1, eventType.getSimpleName());
ResultSet result = stmt.executeQuery();
if (result.next()) {
String evString = result.getString("data");
Event event = Serialization.getJsonMapper().readValue(evString, Event.class);
event.id = result.getLong("id");
event.removed = false;
return Optional.of(event);
} else {
return Optional.empty();
}
}
}
public static Optional<Event> load(Connection connection, long id) throws SQLException, IOException {
try (PreparedStatement statement =
connection.prepareStatement(
"SELECT \"data\",\"removed\" FROM \"events\" WHERE \"id\"=?")) {
statement.setLong(1, id);
ResultSet result = statement.executeQuery();
if (result.next()) {
String evString = result.getString("data");
Event event = Serialization.getJsonMapper().readValue(evString, Event.class);
event.id = id;
event.removed = result.getBoolean("removed");
return Optional.of(event);
} else {
return Optional.empty();
}
}
}
public static List<Event> loadFrom(Connection connection, Instant time) throws SQLException, IOException {
String statement = "SELECT \"id\",\"data\",\"removed\" FROM \"events\" " +
"WHERE \"time\" >= ? ORDER BY \"time\" ASC, \"id\" ASC";
List<Event> events = new ArrayList<>();
try (PreparedStatement stmt = connection.prepareStatement(statement)) {
stmt.setTimestamp(1, Timestamp.from(time));
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
String evString = rs.getString("data");
Event event = Serialization.getJsonMapper().readValue(evString, Event.class);
event.id = rs.getLong("id");
event.removed = rs.getBoolean("removed");
events.add(event);
}
}
return events;
}
public static List<Event> loadAfter(Connection connection, Instant time, long id) throws SQLException, IOException {
String statement = "SELECT \"id\",\"data\",\"removed\" FROM \"events\" " +
"WHERE \"time\" > ? OR (\"time\" = ? AND \"id\" > ?) ORDER BY \"time\" ASC, \"id\" ASC";
List<Event> events = new ArrayList<>();
try (PreparedStatement stmt = connection.prepareStatement(statement)) {
stmt.setTimestamp(1, Timestamp.from(time));
stmt.setTimestamp(2, Timestamp.from(time));
stmt.setLong(3, id);
LOG.debug("Executing query: {}", stmt);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
String evString = rs.getString("data");
Event event = Serialization.getJsonMapper().readValue(evString, Event.class);
event.id = rs.getLong("id");
event.removed = rs.getBoolean("removed");
events.add(event);
}
}
LOG.debug("Loaded {} events", events.size());
return events;
}
public void save(Connection connection) throws SQLException {
if (id != -1) return;
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO \"events\" (\"time\",\"type\",\"data\",\"removed\") " +
"VALUES (?,?,?,?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setTimestamp(1, Timestamp.from(time));
String serialized;
try {
serialized = Serialization.getJsonMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
assert false;
throw new IllegalStateException(e); // TODO(Roel): is this appropriate?
}
statement.setString(2, this.getClass().getSimpleName());
statement.setString(3, serialized);
statement.setBoolean(4, removed);
statement.executeUpdate();
ResultSet keys = statement.getGeneratedKeys();
keys.next();
this.id = keys.getLong(1);
}
}
public void setRemoved(Connection connection, boolean removed) throws SQLException {
if (!isRemovable()) {
assert false; // This is a programming error
return;
}
PreparedStatement statement =
connection.prepareStatement(
"UPDATE \"events\" SET \"removed\"=? WHERE \"id\"=?");
statement.setBoolean(1, removed);
statement.setLong(2, id);
boolean result = statement.execute();
assert(!result);
this.removed = true;
}
}