/*
* Copyright (c) 2010-2016. Axon Framework
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.axonframework.eventhandling.saga;
import java.util.concurrent.Callable;
/**
* Abstract base class of a component that models a saga's life cycle.
*/
public abstract class SagaLifecycle {
private static final ThreadLocal<SagaLifecycle> CURRENT_SAGA_LIFECYCLE = new ThreadLocal<>();
/**
* Registers a AssociationValue with the currently active saga. When the saga is committed, it can be found using
* the registered property. If the saga already has the given association, nothing happens.
*
* @param associationKey The key of the association value to associate this saga with.
* @param associationValue The value of the association value to associate this saga with.
*/
public static void associateWith(String associationKey, String associationValue) {
associateWith(new AssociationValue(associationKey, associationValue));
}
/**
* Registers a AssociationValue with the currently active saga. When the saga is committed, it can be found using
* the registered property. The number value will be converted to a string. If the saga already has the given
* association, nothing happens.
*
* @param associationKey The key of the association value to associate this saga with.
* @param associationValue The value of the association value to associate this saga with.
*/
public static void associateWith(String associationKey, Number associationValue) {
associateWith(new AssociationValue(associationKey, associationValue.toString()));
}
/**
* Registers a AssociationValue with the currently active saga. When the saga is committed, it can be found using
* the registered property. If the saga already has the given association, nothing happens.
*
* @param associationValue The association to associate this saga with.
*/
public static void associateWith(AssociationValue associationValue) {
getInstance().doAssociateWith(associationValue);
}
/**
* Removes the given association from the currently active Saga. When the saga is committed, it can no longer be
* found using the given association value. If the given saga wasn't associated with given values, nothing happens.
*
* @param associationKey The key of the association value to remove from this saga.
* @param associationValue The value of the association value to remove from this saga.
*/
public static void removeAssociationWith(String associationKey, String associationValue) {
getInstance().doRemoveAssociation(new AssociationValue(associationKey, associationValue));
}
/**
* Removes the given association from the currently active Saga. When the saga is committed, it can no longer be
* found using the given association value. If the given saga wasn't associated with given values, nothing happens.
* The number value will be converted to a string.
*
* @param associationKey The key of the association value to remove from this saga.
* @param associationValue The value of the association value to remove from this saga.
*/
public static void removeAssociationWith(String associationKey, Number associationValue) {
removeAssociationWith(associationKey, associationValue.toString());
}
/**
* Marks the saga as ended. Ended saga's may be cleaned up by the repository when they are committed.
*/
public static void end() {
getInstance().doEnd();
}
/**
* {@link SagaLifecycle} instance method to mark the current saga as ended.
*/
protected abstract void doEnd();
/**
* {@link SagaLifecycle} instance method to remove the given {@code associationValue}. If the current saga is not
* associated with given value, this should do nothing.
*
* @param associationValue the association value to remove
*/
protected abstract void doRemoveAssociation(AssociationValue associationValue);
/**
* {@link SagaLifecycle} instance method to register the given {@code associationValue}. If the current saga is
* already associated with given value, this should do nothing.
*
* @param associationValue the association value to add
*/
protected abstract void doAssociateWith(AssociationValue associationValue);
/**
* Get the current {@link SagaLifecycle} instance for the current thread. If none exists an {@link
* IllegalStateException} is thrown.
*
* @return the thread's current {@link SagaLifecycle}
*/
protected static SagaLifecycle getInstance() {
SagaLifecycle instance = CURRENT_SAGA_LIFECYCLE.get();
if (instance == null) {
throw new IllegalStateException("Cannot retrieve current SagaLifecycle; none is yet defined");
}
return instance;
}
/**
* {@link SagaLifecycle} instance method to execute given {@code task} in the context of this SagaLifeCycle. This
* updates the thread's current saga lifecycle before executing the task. If a lifecycle is already registered with
* the current thread that one will be temporarily replaced with this lifecycle until the task completes. This
* method returns the execution result of the task.
*
* @param task the task to execute
* @param <V> the type of execution result of the task
* @return the execution result
* @throws Exception if executing the task results in an exception
*/
protected <V> V executeWithResult(Callable<V> task) throws Exception {
SagaLifecycle existing = CURRENT_SAGA_LIFECYCLE.get();
CURRENT_SAGA_LIFECYCLE.set(this);
try {
return task.call();
} finally {
if (existing == null) {
CURRENT_SAGA_LIFECYCLE.remove();
} else {
CURRENT_SAGA_LIFECYCLE.set(existing);
}
}
}
/**
* {@link SagaLifecycle} instance method to execute given {@code task} in the context of this SagaLifeCycle. This
* updates the thread's current saga lifecycle before executing the task. If a lifecycle is already registered with
* the current thread that one will be temporarily replaced with this lifecycle until the task completes.
*
* @param task the task to execute
*/
protected void execute(Runnable task) {
try {
executeWithResult(() -> {
task.run();
return null;
});
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new SagaExecutionException("Exception while executing a task for a saga", e);
}
}
}