
org.elder.sourcerer.DefaultCommand Maven / Gradle / Ivy
package org.elder.sourcerer;
import com.google.common.collect.ImmutableList;
import org.elder.sourcerer.exceptions.ConflictingExpectedVersionsException;
import org.elder.sourcerer.exceptions.InvalidCommandException;
import org.elder.sourcerer.exceptions.UnexpectedVersionException;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Default Command implementation, expressed in terms of an AggregateRepository.
*/
public class DefaultCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(DefaultCommand.class);
private final AggregateRepository repository;
private final Map metadata;
private Operation super TState, ? super TParams, ? extends TEvent> operation;
private boolean atomic = true;
private boolean idempotentCreate = false;
private String aggregateId = null;
private TParams arguments = null;
private ExpectedVersion expectedVersion = null;
private List metadataDecorators;
public DefaultCommand(
final AggregateRepository repository,
final Operation super TState, ? super TParams, ? extends TEvent> operation) {
this.repository = repository;
this.operation = operation;
this.atomic = operation.atomic();
this.metadata = new HashMap<>();
this.metadataDecorators = new ArrayList<>();
}
@Override
public Command setAggregateId(final String aggregateId) {
this.aggregateId = aggregateId;
return this;
}
@Override
public Command setArguments(final TParams arguments) {
this.arguments = arguments;
return this;
}
@Override
public Command setExpectedVersion(final ExpectedVersion version) {
this.expectedVersion = version;
return this;
}
@Override
public Command setAtomic(final boolean atomic) {
this.atomic = atomic;
return this;
}
@Override
public Command setIdempotentCreate(final boolean idempotentCreate) {
this.idempotentCreate = idempotentCreate;
this.atomic = true;
return this;
}
@Override
public Command addMetadata(final Map metadata) {
this.metadata.putAll(metadata);
return this;
}
@Override
public Command addMetadataDecorator(
final MetadataDecorator metadataDecorator) {
this.metadataDecorators.add(metadataDecorator);
return this;
}
public Command validate() {
if (aggregateId == null) {
throw new InvalidCommandException("No aggregate id specified");
}
if (operation.requiresArguments() && arguments == null) {
throw new InvalidCommandException(
"No arguments specified to command that requires " + "arguments");
}
// Try to calculate effective expected version - will validate combinations
getEffectiveExpectedVersion(expectedVersion, operation.expectedVersion());
// TODO: Add more validation!
return this;
}
@Override
public CommandResult run() {
logger.debug("Running command on {}", aggregateId);
validate();
ExpectedVersion effectiveExpectedVersion
= getEffectiveExpectedVersion(expectedVersion, operation.expectedVersion());
logger.debug("Expected version set as {}", effectiveExpectedVersion);
// Read the aggregate if needed
AggregateRecord aggregateRecord;
if (operation.requiresState() || atomic) {
logger.debug("Reading aggregate record from stream");
aggregateRecord = readAndValidateAggregate(effectiveExpectedVersion);
} else {
aggregateRecord = null;
}
TState aggregate = aggregateRecord == null ? null : aggregateRecord.getAggregate();
logger.debug(
"Current state of aggregate is {}",
aggregateRecord == null
? ""
: "version " + aggregateRecord.getVersion());
// Bail out early if idempotent create, and already present
if (idempotentCreate && aggregateRecord != null && aggregateRecord.getAggregate() != null) {
logger.debug("Bailing out early as already created (and idempotent create set)");
return new CommandResult<>(
aggregateId,
aggregateRecord.getVersion(),
aggregateRecord.getVersion(),
ImmutableList.of());
}
// Execute the command handler
List extends TEvent> operationEvents = operation.execute(aggregate, arguments);
ImmutableList extends TEvent> events
= ImmutableList.copyOf(operationEvents.stream().iterator());
if (events.isEmpty()) {
logger.debug("Operation is no-op, bailing early");
return new CommandResult<>(
aggregateId,
aggregateRecord != null ? aggregateRecord.getVersion() : null,
aggregateRecord != null ? aggregateRecord.getVersion() : null,
events);
}
// Create/update the event stream as needed
ExpectedVersion updateExpectedVersion;
if (atomic) {
// Actually null safe since atomic above ...
if (aggregateRecord.getAggregate() != null) {
updateExpectedVersion = ExpectedVersion.exactly(aggregateRecord.getVersion());
} else {
updateExpectedVersion = ExpectedVersion.notCreated();
}
} else if (idempotentCreate) {
updateExpectedVersion = ExpectedVersion.notCreated();
} else {
updateExpectedVersion = ExpectedVersion.any();
}
// TODO: Handle any existing condition in event store - for now we know it's existing if
// it was existing
if (updateExpectedVersion.getType() == ExpectedVersionType.ANY_EXISTING) {
updateExpectedVersion = ExpectedVersion.any();
}
logger.debug("About to persist, expected version at save: {}", updateExpectedVersion);
Map metadata = new HashMap<>(this.metadata);
for (MetadataDecorator metadataDecorator : metadataDecorators) {
Map decoratorMetadata = metadataDecorator.getMetadata();
if (decoratorMetadata != null) {
metadata.putAll(decoratorMetadata);
}
}
try {
int newVersion = repository.update(
aggregateId,
events,
updateExpectedVersion,
metadata);
// It may be nice to sanity check here by using the expected version explicitly, but
// this works regardless of whether we have a specific expected version ...
// Will return -1 if we just created the stream, which is fine
int oldVersion = newVersion - events.size();
logger.debug("Save successful, new version is {}", newVersion);
return new CommandResult<>(aggregateId, oldVersion, newVersion, events);
} catch (UnexpectedVersionException ex) {
// There's one case when this is OK - idempotent creates. We want to be able to create
// a stream and not fail if the same stream is attempted to be created on replays.
if (idempotentCreate) {
logger.debug("Idempotent create enabled, ignoring existing stream");
return new CommandResult<>(
aggregateId,
ex.getCurrentVersion() != null ? ex.getCurrentVersion() : null,
ex.getCurrentVersion() != null ? ex.getCurrentVersion() : null,
ImmutableList.of());
}
throw ex;
}
}
private static ExpectedVersion getEffectiveExpectedVersion(
final ExpectedVersion commandExpectedVersion,
final ExpectedVersion operationExpectedVersion) {
try {
return ExpectedVersion.merge(commandExpectedVersion, operationExpectedVersion);
} catch (ConflictingExpectedVersionsException ex) {
throw new InvalidCommandException(
"Conflicting expected version constraints: " + ex.getMessage(), ex);
}
}
@NotNull
private AggregateRecord readAndValidateAggregate(
final ExpectedVersion effectiveExpectedVersion) {
AggregateRecord aggregateRecord = repository.read(aggregateId);
// Validate expected version early if we have state
switch (effectiveExpectedVersion.getType()) {
case ANY:
break;
case ANY_EXISTING:
if (aggregateRecord.getAggregate() == null) {
throw new UnexpectedVersionException(-1, effectiveExpectedVersion);
}
break;
case EXACTLY:
if (aggregateRecord.getVersion()
!= effectiveExpectedVersion.getExpectedVersion()) {
throw new UnexpectedVersionException(
aggregateRecord.getVersion(),
effectiveExpectedVersion);
}
break;
case NOT_CREATED:
if (aggregateRecord.getAggregate() != null && !idempotentCreate) {
throw new UnexpectedVersionException(
aggregateRecord.getVersion(),
effectiveExpectedVersion);
}
break;
default:
throw new IllegalArgumentException(
"Unrecognized expected version type " + effectiveExpectedVersion.getType());
}
return aggregateRecord;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy