package com.temenos.interaction.core.hypermedia.transition;
/*
* #%L
* interaction-core
* %%
* Copyright (C) 2012 - 2017 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 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/>.
* #L%
*/
import com.temenos.interaction.core.MultivaluedMapImpl;
import com.temenos.interaction.core.command.*;
import com.temenos.interaction.core.hypermedia.*;
import com.temenos.interaction.core.hypermedia.expression.Expression;
import com.temenos.interaction.core.hypermedia.expression.ExpressionEvaluator;
import com.temenos.interaction.core.resource.RESTResource;
import com.temenos.interaction.core.workflow.TransitionWorkflowStrategyCommandBuilder;
import com.temenos.interaction.core.workflow.WorkflowCommandBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.MultivaluedMap;
import java.util.*;
import static com.temenos.interaction.core.command.InteractionCommand.*;
/**
* Transitions an {@link InteractionContext} into another {@link ResourceState}
* according to the auto {@link Transition} tree defined for its current {@link ResourceState}.
* During this process it gathers path parameters, query parameters, context attributes
* and resource entities of each successful auto {@link Transition} that it processes
* into {@link InteractionContext}.
*
* @author ikarady
*/
public class AutoTransitioner {
private static final Logger LOGGER = LoggerFactory.getLogger(AutoTransitioner.class);
private InteractionContext originalCtx = null;
private Transformer transformer = null;
private WorkflowCommandBuilder workflowCommandBuilder = null;
private ResourceLocatorProvider resourceLocatorProvider = null;
private LazyResourceStateResolver lazyResourceStateResolver = null;
private ResourceParameterResolverProvider parameterResolverProvider = null;
private ExpressionEvaluator expressionEvaluator = null;
private int stateRevisitLimit = 100;
private Outcome outcome = null;
public AutoTransitioner(InteractionContext ctx, Transformer transformer, CommandController commandController,
ResourceLocatorProvider resourceLocatorProvider, LazyResourceStateResolver lazyResourceStateResolver) {
this.originalCtx = copyInteractionContext(ctx);
this.transformer = transformer;
this.workflowCommandBuilder = new TransitionWorkflowStrategyCommandBuilder(commandController);
this.resourceLocatorProvider = resourceLocatorProvider;
this.lazyResourceStateResolver = lazyResourceStateResolver;
}
public AutoTransitioner setParameterResolverProvider(ResourceParameterResolverProvider parameterResolverProvider) {
this.parameterResolverProvider = parameterResolverProvider;
return this;
}
public AutoTransitioner setWorkflowCommandBuilder(WorkflowCommandBuilder workflowCommandBuilder) {
this.workflowCommandBuilder = workflowCommandBuilder;
return this;
}
public AutoTransitioner setExpressionEvaluator(ExpressionEvaluator expressionEvaluator) {
this.expressionEvaluator = expressionEvaluator;
return this;
}
/**
* Returns the {@link Outcome Outcome} of auto transitioning.
*
* @return {@link Outcome Outcome}
*
*/
public Outcome getOutcome() {
return outcome;
}
/**
* Traverses through a tree of auto transitions defined for the current {@link ResourceState}
* in {@link InteractionContext} to find one successful branch.
* At each level in the tree it picks the first successful auto {@link Transition} ignoring the rest.
* Hence a successful branch is made up of auto {@link Transition}s that represent the first successful
* auto {@link Transition} of their level.
* Traversal stops when there are no more successful auto transitions at any given level.
* The result of a successful auto {@link Transition} is a new {@link ResourceState}
* which is saved into the overall {@link Outcome Outcome} along with any path parameters, query parameters,
* context attributes and resource entities.
*
* @return {@link Outcome Outcome}
*
*/
public Outcome transition() {
if (outcome != null) {
return outcome;
}
outcome = new Outcome();
outcome.setRestResource(originalCtx.getResource());
outcome.addPathParameters(copyParameters(originalCtx.getPathParameters()));
outcome.addQueryParameters(copyParameters(originalCtx.getQueryParameters()));
outcome.addCtxAttributes(copyProperties(originalCtx.getAttributes()));
outcome.addEntityProperties(originalCtx.getResource());
try {
for (Transition autoTransition : originalCtx.getCurrentState().getAutoTransitions()) {
if(transition(autoTransition)) {
return outcome;
}
}
} catch (ResourceStateRevisitedException e) {
LOGGER.error("Auto transitioned into same resource state multiple times", e);
}
return outcome;
}
private boolean transition(Transition transition) throws ResourceStateRevisitedException {
Outcome currentOutcome = new Outcome(outcome);
currentOutcome.setState(lazyResourceStateResolver.resolve(transition.getTarget()));
currentOutcome.addTransitionProperties(transition);
currentOutcome.setExpression(transition.getCommand().getEvaluation());
if (transition.getTarget() instanceof DynamicResourceState) {
DynamicResourceStateResolver dynamicStateResolver = new DynamicResourceStateResolver((DynamicResourceState) transition.getTarget(), resourceLocatorProvider)
.setParameterResolverProvider(parameterResolverProvider)
.addProperties(currentOutcome.getTransitionPropertiesBuilder().build())
.addProperties(currentOutcome.getCtxAttributes())
.addPathParameters(currentOutcome.getPathParameters())
.addQueryParameters(currentOutcome.getQueryParameters());
ResourceStateAndParameters stateAndParameters = dynamicStateResolver.resolve();
if (stateAndParameters == null) {
return false;
}
currentOutcome.addPathParameters(dynamicStateResolver.getPathParameters());
currentOutcome.addQueryParameters(dynamicStateResolver.getQueryParameters());
currentOutcome.setState(lazyResourceStateResolver.resolve(stateAndParameters.getState()));
}
if (currentOutcome.getState() == null) {
return false;
}
currentOutcome.addCommand(currentOutcome.buildCommand());
InteractionContext ctx = currentOutcome.getInteractionContext();
currentOutcome.evaluate(ctx);
if (currentOutcome.isSuccessful() || currentOutcome.isInterim()) {
currentOutcome.setRestResource(ctx.getResource());
currentOutcome.addOutQueryParameters(ctx.getOutQueryParameters());
currentOutcome.addCtxAttributes(ctx.getAttributes());
currentOutcome.addEntityProperties(ctx.getResource());
outcome.add(currentOutcome);
for (Transition autoTransition : currentOutcome.getState().getAutoTransitions()) {
if (transition(autoTransition)) {
currentOutcome.setInterimSuccessful(true);
return true;
}
}
currentOutcome.setInterimSuccessful(false);
}
return currentOutcome.isSuccessful();
}
protected InteractionContext copyInteractionContext(InteractionContext ctx) {
InteractionContext ctxCopy = new InteractionContext(
ctx,
ctx.getHeaders(),
copyParameters(ctx.getPathParameters()),
copyParameters(ctx.getQueryParameters()),
ctx.getCurrentState());
ctxCopy.getResponseHeaders().putAll(ctx.getResponseHeaders());
return ctxCopy;
}
protected MultivaluedMap<String, String> copyParameters(MultivaluedMap<String, String> parameters) {
MultivaluedMap<String, String> parametersCopy = new MultivaluedMapImpl<>();
parametersCopy.putAll(parameters);
return parametersCopy;
}
protected Map<String, Object> copyProperties(Map<String, Object> properties) {
Map<String, Object> propertiesCopy = new HashMap<>();
propertiesCopy.putAll(properties);
return propertiesCopy;
}
protected MultivaluedMap<String, String> toParameters(Map<String, Object> properties) {
MultivaluedMap<String, String> parameters = new MultivaluedMapImpl<>();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
if (properties.get(entry.getKey()) != null)
parameters.add(entry.getKey(), entry.getValue().toString());
}
return parameters;
}
/**
* Represents the outcome of auto transitioning process which can be either successful or not.
* If successful a new {@link InteractionContext} can be requested with updated current {@link ResourceState},
* path parameters, query parameters, context attributes and resource entity.
*
* @author ikarady
*/
public class Outcome {
private TransitionPropertiesBuilder transitionPropertiesBuilder = new TransitionPropertiesBuilder(transformer);
private MultivaluedMap<String, String> outQueryParameters = new MultivaluedMapImpl<>();
private Map<String, Object> ctxAttributes = new HashMap<>();
private RESTResource restResource = null;
private Boolean isSuccessful = null;
private ResourceState state = null;
private Set<VisitedState> visitedStates = new HashSet<>();
private Expression expression = null;
private InteractionCommand command = null;
private InteractionCommand delayedCommand = null;
private Outcome interimOutcome = null;
private Outcome() {}
private Outcome (Outcome other) {
if (other.getInterimOutcome() != null && other.getInterimOutcome().isPending()) {
this.apply(other.getInterimOutcome());
} else {
this.apply(other);
}
}
/**
* Returns true if the auto transitioning process was a success overall otherwise false.
* Success means it had at least one successful auto {@link Transition}.
*
* @return true or false
*
*/
public boolean isSuccessful() {
return isSuccessful != null && isSuccessful;
}
/**
* Returns a new {@link InteractionContext} with updated current {@link ResourceState},
* path parameters, query parameters, context attributes and resource entity.
*
* @return {@link InteractionContext}
*
*/
public InteractionContext getInteractionContext() {
InteractionContext ctx = new InteractionContext(
originalCtx,
originalCtx.getHeaders(),
copyParameters(toParameters(transitionPropertiesBuilder.build())),
copyParameters(getQueryParameters()),
getState());
ctx.setTargetState(ctx.getCurrentState());
ctx.setResource(getRestResource());
ctx.getAttributes().putAll(getCtxAttributes());
ctx.getOutQueryParameters().putAll(getOutQueryParameters());
ctx.getResponseHeaders().putAll(originalCtx.getResponseHeaders());
return ctx;
}
private boolean isPending() {
return isSuccessful == null;
}
private TransitionPropertiesBuilder getTransitionPropertiesBuilder() {
return new TransitionPropertiesBuilder(transitionPropertiesBuilder);
}
private void setTransitionPropertiesBuilder(TransitionPropertiesBuilder transitionPropertiesBuilder) {
this.transitionPropertiesBuilder = transitionPropertiesBuilder;
}
private MultivaluedMap<String, String> getPathParameters() {
return transitionPropertiesBuilder.getPathParameters();
}
private void addPathParameters(MultivaluedMap<String, String> pathParameters) {
this.transitionPropertiesBuilder.addPathParameters(pathParameters);
}
private MultivaluedMap<String, String> getQueryParameters() {
return transitionPropertiesBuilder.getQueryParameters();
}
private void addQueryParameters(MultivaluedMap<String, String> queryParameters) {
this.transitionPropertiesBuilder.addQueryParameters(queryParameters);
}
private MultivaluedMap<String, String> getOutQueryParameters() {
return outQueryParameters;
}
private void addOutQueryParameters(MultivaluedMap<String, String> outQueryParameters) {
this.outQueryParameters.putAll(outQueryParameters);
}
private Map<String, Object> getCtxAttributes() {
return ctxAttributes;
}
private void addCtxAttributes(Map<String, Object> ctxAttributes) {
this.ctxAttributes.putAll(ctxAttributes);
}
private void addEntityProperties(RESTResource restResource) {
this.transitionPropertiesBuilder.addRESTResource(restResource);
}
private void addTransitionProperties(Transition transition) {
this.transitionPropertiesBuilder.addTransition(transition);
}
private RESTResource getRestResource() {
return restResource;
}
private void setRestResource(RESTResource restResource) {
this.restResource = restResource;
}
private void setSuccessful(Boolean isSuccessful) {
this.isSuccessful = isSuccessful;
}
private void setInterimSuccessful(Boolean isSuccessful) {
if (isInterim()) {
setSuccessful(isSuccessful);
}
}
private void addResult(Result result) {
if (!isInterim()) {
this.isSuccessful = result == null || result.equals(Result.SUCCESS) || result.equals(Result.CREATED);
}
}
private boolean isInterim() {
return delayedCommand != null;
}
private ResourceState getState() {
return state;
}
private void setState(ResourceState state) {
this.state = state;
}
private Expression getExpression() {
return expression;
}
private void setExpression(Expression expression) {
this.expression = expression;
}
private InteractionCommand getDelayedCommand() {
return delayedCommand;
}
private void setCommand(InteractionCommand command) {
this.command = command;
}
private void addCommand(InteractionCommand command) {
if (command == null) {
return;
}
if (command instanceof TransitionCommand && ((TransitionCommand)command).isInterim()) {
this.delayedCommand = command;
} else {
this.command = workflowCommandBuilder.build(new InteractionCommand[] {this.command, command});
}
}
private InteractionCommand buildCommand() {
return workflowCommandBuilder.build(state.getActions());
}
private void evaluate(InteractionContext ctx) {
try {
if (expressionEvaluator != null && expression != null && !expressionEvaluator.evaluate(expression, ctx, null)) {
addResult(Result.FAILURE);
} else {
addResult((command == null) ? Result.SUCCESS : command.execute(ctx));
}
} catch (InteractionException ie) {
LOGGER.error("Transition command on state [{}] failed with error [{} - {}]: ",
state.getId(), ie.getHttpStatus(), ie.getHttpStatus().getReasonPhrase(), ie);
addResult(Result.FAILURE);
}
}
private VisitedState visitState(ResourceState state) throws ResourceStateRevisitedException {
for (VisitedState visitedState : visitedStates) {
if (visitedState.visit(state)) {
return visitedState;
}
}
VisitedState visitedState = new VisitedState(state);
visitedStates.add(visitedState);
return visitedState;
}
private Set<VisitedState> getVisitedStates() {
return visitedStates;
}
private void setVisitedStates(Set<VisitedState> visitedStates) {
this.visitedStates = visitedStates;
}
private Outcome getInterimOutcome() {
return interimOutcome;
}
private void setInterimOutcome(Outcome interimOutcome) {
this.interimOutcome = interimOutcome;
}
private void add(Outcome other) throws ResourceStateRevisitedException {
if (other == null) {
return;
}
if (other.isInterim()) {
setInterimOutcome(other);
} else {
apply(other);
setSuccessful(other.isSuccessful());
if (visitState(other.getState()).getCount() > stateRevisitLimit) {
throw new ResourceStateRevisitedException("Resource state "+state+" has been revisited more than "+stateRevisitLimit+" times");
}
}
}
private void apply(Outcome other) {
if (other == null) {
return;
}
setTransitionPropertiesBuilder(other.getTransitionPropertiesBuilder());
addOutQueryParameters(other.getOutQueryParameters());
addCtxAttributes(other.getCtxAttributes());
setRestResource(other.getRestResource());
setState(other.getState());
setCommand(other.getDelayedCommand());
setVisitedStates(other.getVisitedStates());
setInterimOutcome(other.getInterimOutcome());
}
}
private class VisitedState {
private ResourceState state = null;
private int count = 0;
private VisitedState(ResourceState state) {
this.state = state;
this.count = 1;
}
private int getCount() {
return count;
}
private boolean visit(ResourceState state) {
if (this.state.equals(state)) {
count++;
return true;
}
return false;
}
}
}