package com.temenos.interaction.core.hypermedia;
/*
* #%L
* interaction-core
* %%
* Copyright (C) 2012 - 2013 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 java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import com.temenos.interaction.core.hypermedia.expression.Expression;
import com.temenos.interaction.core.hypermedia.transition.TransitionPropertiesBuilder;
import com.temenos.interaction.core.workflow.*;
import com.temenos.interaction.core.workflow.WorkflowCommandBuilderProvider.WorkflowType;
import org.odata4j.core.OEntity;
import org.odata4j.core.OEntityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.temenos.interaction.core.MapWithReadWriteLock;
import com.temenos.interaction.core.MultivaluedMapImpl;
import com.temenos.interaction.core.cache.Cache;
import com.temenos.interaction.core.command.CommandController;
import com.temenos.interaction.core.command.CommonAttributes;
import com.temenos.interaction.core.command.InteractionCommand;
import com.temenos.interaction.core.command.InteractionContext;
import com.temenos.interaction.core.command.InteractionException;
import com.temenos.interaction.core.entity.Entity;
import com.temenos.interaction.core.entity.EntityMetadata;
import com.temenos.interaction.core.entity.EntityProperty;
import com.temenos.interaction.core.entity.Metadata;
import com.temenos.interaction.core.resource.CollectionResource;
import com.temenos.interaction.core.resource.EntityResource;
import com.temenos.interaction.core.resource.MetaDataResource;
import com.temenos.interaction.core.resource.RESTResource;
import com.temenos.interaction.core.rim.HTTPHypermediaRIM;
import com.temenos.interaction.core.rim.ResourceRequestConfig;
import com.temenos.interaction.core.rim.ResourceRequestHandler;
import com.temenos.interaction.core.rim.ResourceRequestResult;
import com.temenos.interaction.core.rim.SequentialResourceRequestHandler;
/**
* A state machine that is responsible for creating the links (hypermedia) to
* other valid application states.
*
* @author aphethean
*
*/
public class ResourceStateMachine {
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceStateMachine.class);
// members
ResourceState initial;
ResourceState exception;
Transformer transformer;
CommandController commandController;
Cache responseCache;
ResourceStateProvider resourceStateProvider;
ResourceLocatorProvider resourceLocatorProvider;
ResourceParameterResolverProvider parameterResolverProvider;
WorkflowCommandBuilderProvider workflowCommandBuilderProvider;
// optimised access
private Map<String, Transition> transitionsById = new MapWithReadWriteLock<String, Transition>();
private Map<String, Transition> transitionsByRel = new MapWithReadWriteLock<String, Transition>();
private Map<String, Set<String>> interactionsByPath = new MapWithReadWriteLock<String, Set<String>>();
private Map<String, Set<String>> interactionsByState = new MapWithReadWriteLock<String, Set<String>>();
private Map<String, Set<String>> resourceStateNamesByPath = new MapWithReadWriteLock<String, Set<String>>();
private Map<String, ResourceState> resourceStatesByName = new MapWithReadWriteLock<String, ResourceState>();
public ResourceStateMachine(ResourceState initialState) {
this(initialState, null, null, null);
}
public ResourceStateMachine(ResourceState initialState, ResourceLocatorProvider resourceLocatorProvider) {
this(initialState, null, null, resourceLocatorProvider, null);
}
public ResourceStateMachine(ResourceState initialState, ResourceState exceptionState) {
this(initialState, exceptionState, null, null);
}
public ResourceStateMachine(ResourceState initialState, ResourceState exceptionState,
ResourceLocatorProvider resourceLocatorProvider) {
this(initialState, exceptionState, null, resourceLocatorProvider, null);
}
public CommandController getCommandController() {
return commandController;
}
public void setCommandController(CommandController commandController) {
this.commandController = commandController;
}
public Cache getCache() {
return responseCache;
}
public void setCache(Cache cache) {
responseCache = cache;
}
// TODO support Event
public InteractionCommand determineAction(Event event, String resourcePath) {
List<Action> actions = new ArrayList<Action>();
Set<ResourceState> resourceStates = getResourceStatesByPath().get(resourcePath);
for (ResourceState s : resourceStates) {
actions.addAll(determineActions(event, s));
}
return buildWorkflow(event, actions);
}
public List<Action> determineActions(Event event, ResourceState state) {
List<Action> actions = new ArrayList<Action>();
Set<String> interactions = getInteractionByState().get(state.getName());
// TODO turn interactions into Events
if (interactions.contains(event.getMethod())) {
for (Action a : state.getActions()) {
if (event.isSafe() && a.getType().equals(Action.TYPE.VIEW)) {
// Add action to list. Since we now support command chains,
// with more than one GET command, it is possible
// to have more than one VIEW in the action list.
actions.add(a);
} else if (event.isUnSafe() && a.getType().equals(Action.TYPE.ENTRY)
&& (a.getMethod() == null || event.getMethod().equals(a.getMethod()))) {
actions.add(a);
}
}
}
return actions;
}
public InteractionCommand buildWorkflow(Event event, List<Action> actions) {
assert (event != null);
WorkflowCommand command = getWorkflowCommandBuilder(WorkflowType.INTERACTION).build(actions);
return !command.isEmpty() ? command : null;
}
public WorkflowCommandBuilder getWorkflowCommandBuilder(WorkflowType workflowType) {
if (workflowCommandBuilderProvider == null) {
return new AbortOnErrorWorkflowStrategyCommandBuilder(commandController);
}
return workflowCommandBuilderProvider.getBuilder(workflowType);
}
public ResourceState determineState(Event event, String resourcePath) {
ResourceState state = null;
Set<ResourceState> resourceStates = getResourceStatesByPath().get(resourcePath);
if (resourceStates != null) {
for (ResourceState s : resourceStates) {
Set<String> interactions = getInteractionByState().get(s.getName());
if (interactions.contains(event.getMethod())) {
if (state == null || interactions.size() == 1 || !event.getMethod().equals("GET")) { // Avoid
// overriding
// existing
// view
// actions
if (state != null && state.getViewAction() != null) {
LOGGER.error("Multiple matching resource states for [{}] event on [{}], [{}] and [{}]",
event, resourcePath, state, s );
}
state = s;
}
}
}
}
return state;
}
/**
*
* @invariant initial state not null
* @param initialState
* @param transformer
*/
public ResourceStateMachine(ResourceState initialState, Transformer transformer) {
this(initialState, null, transformer, null);
}
public ResourceStateMachine(ResourceState initialState, Transformer transformer,
ResourceStateProvider resourceStateProvider) {
this(initialState, null, transformer, null, resourceStateProvider);
}
/**
*
* @invariant initial state not null
* @param initialState
* @param transformer
*/
public ResourceStateMachine(ResourceState initialState, Transformer transformer,
ResourceLocatorProvider resourceLocatorProvider) {
this(initialState, null, transformer, resourceLocatorProvider, null);
}
public ResourceStateMachine(ResourceState initialState, ResourceState exceptionState, Transformer transformer) {
this(initialState, exceptionState, transformer, null, null);
}
public ResourceStateMachine(ResourceState initialState, ResourceState exceptionState, Transformer transformer,
ResourceStateProvider resourceStateProvider) {
this(initialState, exceptionState, transformer, null, resourceStateProvider);
}
public ResourceStateMachine(ResourceState initialState, ResourceState exceptionState, Transformer transformer,
ResourceLocatorProvider resourceLocatorProvider, ResourceStateProvider resourceStateProvider) {
if (initialState == null)
throw new RuntimeException("Initial state must be supplied");
LOGGER.info("Constructing ResourceStateMachine with initial state [{}]", initialState);
assert (exceptionState == null || exceptionState.isException());
this.initial = initialState;
this.initial.setInitial(true);
this.exception = exceptionState;
this.transformer = transformer;
this.resourceLocatorProvider = resourceLocatorProvider;
this.resourceStateProvider = resourceStateProvider;
build();
}
public void setWorkflowCommandBuilderProvider(WorkflowCommandBuilderProvider workflowCommandBuilderProvider) {
this.workflowCommandBuilderProvider = workflowCommandBuilderProvider;
}
/**
* This method is called during resource state machine construction and
* builds the resource state machine's internal state graph starting from
* the initial state.
*/
private synchronized void build() {
registerAllStartingFromState(initial, HttpMethod.GET);
}
/**
* Starting from the given state / method pair, fully initialises the machine's
* internal state graph. Already registered states will not be processed, as well
* as its children states.
*
* This method serves as a replacement for all the collect*By* methods, which
* purpose was to initialise the optimised access maps.
*
* @precondition The pair state / method to start with should NOT be already registered,
* as well as none of its children, and the state should not be null
* @invariant Given state not null
* @postcondition All reachable states from the given state should be registered,
* regardless of the method
* @param state
* The starting resource state from where to register
* @param method
* The HTTP method associated with the state, usually the default GET
* method
*/
public synchronized void registerAllStartingFromState(ResourceState state, String method) {
checkAndResolve(state);
if (state == null) return;
populateAccessMaps(state, method);
// don't register any further if the current state was already processed
if(resourceStatesByName.containsKey(state.getName())) return;
resourceStatesByName.put(state.getName(), state);
// Register all target resources from this resource
for (Transition tmpTransition : state.getTransitions()) {
if(tmpTransition.getTarget() != null) {
registerAllStartingFromState(tmpTransition.getTarget(), tmpTransition.getCommand().getMethod());
}
}
}
/**
* Registers the given state / method pair, and any states required to
* process the given state, with the resource state machine's internal state
* graph
*
* @precondition The pair state / method to register, where the state should not be null
* @invariant Given state not null
* @postcondition All target states from the given state with a transition that is either
* EMBEDDED, FOR_EACH or FOR_EACH_EMBEDDED should be registered,
* regardless of the method
* @param state
* The resource state to register
* @param method
* The HTTP method associated with the state, this is important
* as the state to handle a request is determined by the duo of
* the state's path and HTTP method; path alone is not sufficient
* as multiple states can share the same path
*/
public synchronized void register(ResourceState state, String method) {
checkAndResolve(state);
if (state == null) return;
populateAccessMaps(state, method);
// don't register any further if the current state was already processed
if(resourceStatesByName.containsKey(state.getName())) return;
resourceStatesByName.put(state.getName(), state);
// Register any embedded / foreach resources linked to this resource
for (Transition tmpTransition : state.getTransitions()) {
if(tmpTransition.getTarget() != null) {
if (tmpTransition.isAnyOfTypes(Transition.EMBEDDED, Transition.FOR_EACH, Transition.FOR_EACH_EMBEDDED)) {
register(tmpTransition.getTarget(), tmpTransition.getCommand().getMethod());
}
}
}
}
/**
* Maps should be populated for a state / method pair, even if the state was already
* processed, since we can reach a state by different methods.
*/
private void populateAccessMaps(ResourceState state, String method) {
collectTransitionsByIdForState(state);
collectTransitionsByRelForState(state);
collectInteractionsByPathForState(state, method);
collectInteractionsByStateForState(state, method);
collectResourceStatesByPathForState(state);
}
/**
* @param state
*/
private void collectResourceStatesByPathForState(ResourceState state) {
Set<String> resourceStateNames = resourceStateNamesByPath.get(state.getResourcePath());
if (resourceStateNames == null) {
resourceStateNames = new HashSet<String>();
resourceStateNamesByPath.put(state.getResourcePath(), resourceStateNames);
}
resourceStateNames.add(state.getName());
}
/**
* @param state
* @param method
*/
private void collectInteractionsByStateForState(ResourceState state, String method) {
Set<String> stateInteractions = interactionsByState.get(state.getName());
if (stateInteractions == null) {
stateInteractions = new HashSet<String>();
interactionsByState.put(state.getName(), stateInteractions);
}
if (!state.isPseudoState()) {
if (method != null) {
stateInteractions.add(method);
} else {
stateInteractions.add(HttpMethod.GET);
}
}
if (state.getActions() != null) {
for (Action action : state.getActions()) {
if (action.getMethod() != null) {
stateInteractions.add(action.getMethod());
}
}
}
for (ResourceState next : state.getAllTargets()) {
List<Transition> transitions = state.getTransitions(next);
for (Transition t : transitions) {
TransitionCommandSpec command = t.getCommand();
Set<String> tmpStateInteractions = interactionsByState.get(next.getName());
if (tmpStateInteractions == null) {
tmpStateInteractions = new HashSet<String>();
interactionsByState.put(next.getName(), tmpStateInteractions);
}
if (command.getMethod() != null && !command.isAutoTransition())
tmpStateInteractions.add(command.getMethod());
}
}
}
/**
* @param state
* @param method
*/
private void collectInteractionsByPathForState(ResourceState state, String method) {
Set<String> pathInteractions = interactionsByPath.get(state.getPath());
if (pathInteractions == null) {
pathInteractions = new HashSet<String>();
interactionsByPath.put(state.getPath(), pathInteractions);
}
if (method != null) {
pathInteractions.add(method);
} else {
pathInteractions.add(HttpMethod.GET);
}
}
/**
* @param state
*/
private void collectTransitionsByRelForState(ResourceState state) {
for (Transition transition : state.getTransitions()) {
if (transition == null) {
LOGGER.debug("collectTransitionsByRel : null transition detected");
} else if (transition.getTarget() == null) {
LOGGER.debug("collectTransitionsByRel : null target detected");
} else if (transition.getTarget().getRel() == null) {
LOGGER.debug("collectTransitionsByRel : null relation detected");
} else {
transitionsByRel.put(transition.getTarget().getRel(), transition);
}
}
}
/**
* @param state
*/
private void collectTransitionsByIdForState(ResourceState state) {
for (Transition transition : state.getTransitions()) {
transitionsById.put(transition.getId(), transition);
}
}
/**
* Unregisters the given state / method pair from the resource state
* machine's internal state graph
*
* @precondition A registered pair state / method, where the state should not be null
* @invariant Given state not null
* @postcondition The state is not reachable by the unregistered method
* @param state
* The resource state to unregister
* @param method
* The HTTP method associated with the state, this is important
* as the state to handle a request is determined by the duo of
* the state's path and HTTP method; path alone is not sufficient
* as multiple states can share the same path
*/
public synchronized void unregister(ResourceState state, String method) {
if(state == null) return;
// don't do anything if the state is not registered
if(!resourceStatesByName.containsKey(state.getName())) return;
for (Transition transition : state.getTransitions()) {
// remove transitions originating in state for this method only
if(transition.getCommand().getMethod() == method)
transitionsById.remove(transition.getId());
// remove transitions originating in state for this method only
if (transition.getTarget() != null) {
if(transition.getCommand().getMethod() == method)
transitionsByRel.remove(transition.getTarget().getRel());
}
}
// Process interactions by path
final Set<String> pathInteractions = interactionsByPath.get(state.getPath());
if (pathInteractions != null)
pathInteractions.remove(method);
// Process interactions by state
final Set<String> stateInteractions = interactionsByState.get(state.getName());
if (stateInteractions != null)
stateInteractions.remove(method);
// only remove resources by path and by name if there are no methods associated with it
if(stateInteractions != null)
if(stateInteractions.isEmpty()) {
// Process resource states by path
final Set<String> pathStateNames = resourceStateNamesByPath.get(state.getResourcePath());
if (pathStateNames != null) {
pathStateNames.remove(state.getName());
}
resourceStatesByName.remove(state.getName());
}
}
public void setParameterResolverProvider(ResourceParameterResolverProvider parameterResolverProvider) {
this.parameterResolverProvider = parameterResolverProvider;
}
public ResourceState getInitial() {
return initial;
}
public ResourceState getException() {
return exception;
}
public Transformer getTransformer() {
return transformer;
}
public synchronized Collection<ResourceState> getStates() {
return Collections.unmodifiableCollection(resourceStatesByName.values());
}
/**
* Return a map of all the paths, and interactions with those states mapped
* to that path
*
* @return
*/
public Map<String, Set<String>> getInteractionByPath() {
return interactionsByPath;
}
/**
* Return a map of all the ResourceState's, and interactions with those
* states.
*
* @return
*/
public Map<String, Set<String>> getInteractionByState() {
return interactionsByState;
}
/**
* For a given resource state, get the valid interactions.
*
* @param state
* @return
*/
public Set<String> getInteractions(ResourceState state) {
Set<String> interactions = null;
if (state != null) {
assert (getStates().contains(state));
Map<String, Set<String>> interactionMap = getInteractionByPath();
interactions = interactionMap.get(state.getPath());
}
return interactions;
}
/**
* For a given path, return the resource states.
*
* @param path
* @return
*/
public Set<ResourceState> getResourceStatesForPath(String path) {
if (path == null) {
path = initial.getPath();
}
return getResourceStatesByPath().get(path);
}
/**
* For a given path regular expression, return the resource states.
*
* @param pathRegex
* @return
*/
public Set<ResourceState> getResourceStatesForPathRegex(String pathRegex) {
if (pathRegex == null) {
pathRegex = initial.getPath();
}
return getResourceStatesForPathRegex(Pattern.compile(pathRegex));
}
/**
* Return a set of resources based on a pattern, without
* ensuring consistency between the returned set and the state
* of the internal maps.
*
* @see {@link ResourceStateMachine#getResourceStatesForPathRegex(String)}
*/
public Set<ResourceState> getResourceStatesForPathRegex(Pattern pattern) {
Set<ResourceState> matchingStates = new HashSet<ResourceState>();
for (String path : resourceStateNamesByPath.keySet()) {
Matcher m = pattern.matcher(path);
if (m.matches()) {
matchingStates.addAll(getResourceStatesForPath(path));
}
}
return matchingStates;
}
/**
* Return a map of all the paths to the various resources, without
* ensuring consistency between the returned map and the state
* of the internal maps.
*
* @invariant initial state not null
*/
public Map<String, Set<ResourceState>> getResourceStatesByPath() {
Map<String, Set<ResourceState>> stateMap = new HashMap<String, Set<ResourceState>>();
for (Entry<String, Set<String>> entry : resourceStateNamesByPath.entrySet()) {
Set<ResourceState> resourceStateSet = new LinkedHashSet<ResourceState>();
for(String resourceStateName : entry.getValue()) {
ResourceState state = resourceStatesByName.get(resourceStateName);
if(state != null) resourceStateSet.add(state);
}
stateMap.put(entry.getKey(), resourceStateSet);
}
return stateMap;
}
/**
* Return a map of all the paths to the sub states from the supplied
* ResourceState.
*
* @precondition begin state not null
* @invariant initial state not null
*/
public Map<String, Set<ResourceState>> getResourceStatesByPath(ResourceState begin) {
assert (begin != null);
collectResourceStatesByPath(resourceStateNamesByPath, begin);
return getResourceStatesByPath();
}
private void collectResourceStatesByPath(Map<String, Set<String>> result, ResourceState begin) {
List<ResourceState> states = new ArrayList<ResourceState>();
collectResourceStatesByPath(result, states, begin);
}
private void collectResourceStatesByPath(Map<String, Set<String>> result, Collection<ResourceState> states,
ResourceState currentState) {
if (currentState == null) {
return;
}
for (ResourceState tmpState : states) {
if (tmpState == currentState)
return;
}
states.add(currentState);
// add current state to results
Set<String> thisStateSet = result.get(currentState.getResourcePath());
if (thisStateSet == null)
thisStateSet = new HashSet<String>();
thisStateSet.add(currentState.getName());
result.put(currentState.getResourcePath(), thisStateSet);
for (ResourceState next : currentState.getAllTargets()) {
if (next != null && next != currentState) {
String path = next.getResourcePath();
if (result.get(path) != null) {
if (!result.get(path).contains(next.getName())) {
LOGGER.debug("Adding to existing ResourceState[{}] set ({}): {}", path, result.get(path), next);
result.get(path).add(next.getName());
}
} else {
LOGGER.debug("Putting a ResourceState[{}]: {}", path, next);
Set<String> set = new HashSet<String>();
set.add(next.getName());
result.put(path, set);
}
}
collectResourceStatesByPath(result, states, next);
}
}
/**
* For a given state name, return the resource state.
*
* @param name
* @return
*/
public ResourceState getResourceStateByName(String name) {
if (name == null)
throw new IllegalArgumentException("State name not supplied");
return getResourceStateByName().get(name);
}
/**
* Return a map of all the state names to ResourceState
*
* @invariant initial state not null
* @return
*/
public Map<String, ResourceState> getResourceStateByName() {
return resourceStatesByName;
}
/**
* Return a map of all the state names to the sub states from the supplied
* ResourceState.
*
* @precondition begin state not null
* @invariant initial state not null
*/
public Map<String, ResourceState> getResourceStateByName(ResourceState begin) {
assert (begin != null);
Map<String, ResourceState> stateMap = new HashMap<String, ResourceState>();
collectResourceStatesByName(stateMap, begin);
return stateMap;
}
private void collectResourceStatesByName(Map<String, ResourceState> result, ResourceState begin) {
List<ResourceState> states = new ArrayList<ResourceState>();
collectResourceStatesByName(result, states, begin);
}
private void collectResourceStatesByName(Map<String, ResourceState> result, Collection<ResourceState> states,
ResourceState currentState) {
if (currentState == null)
return;
for (ResourceState tmpState : states) {
if (tmpState == currentState)
return;
}
states.add(currentState);
// add current state to results
result.put(currentState.getName(), currentState);
for (ResourceState next : currentState.getAllTargets()) {
if (next != null && next != currentState) {
String name = next.getName();
LOGGER.debug("Putting a ResourceState[{}]: {}", name, next);
result.put(name, next);
}
collectResourceStatesByName(result, states, next);
}
}
/**
* Evaluate and return all the valid links (target states) from this
* resource state.
*
* @param rimHandler
* @param ctx
* @param resourceEntity
* @return
*/
public Collection<Link> injectLinks(HTTPHypermediaRIM rimHandler, InteractionContext ctx,
RESTResource resourceEntity, HttpHeaders headers, Metadata metadata) {
return injectLinks(rimHandler, ctx, resourceEntity, null, headers, metadata);
}
/**
* Evaluate and return all the valid links (target states) from the current
* resource state (@see {@link InteractionContext#getCurrentState()}).
*
* @param rimHander
* @param ctx
* @param resourceEntity
* @param selfTransition
* if we are injecting links into a resource that has resulted
* from a transition from another resource (e.g an auto
* transition or an embedded transition) then we need to use the
* transition parameters as there are no path parameters
* available. i.e. because we've not made a request for this
* resource through the whole jax-rs stack
* @return
*/
public Collection<Link> injectLinks(HTTPHypermediaRIM rimHander, InteractionContext ctx,
RESTResource resourceEntity, Transition selfTransition, HttpHeaders headers, Metadata metadata) {
// Add path and query parameters to the list of resource properties
MultivaluedMap<String, String> resourceProperties = new MultivaluedMapImpl<String>();
resourceProperties.putAll(ctx.getPathParameters());
if (null != ctx.getAttribute(CommonAttributes.O_DATA_ENTITY_ATTRIBUTE)) {
resourceProperties.putSingle("profileOEntity", (String) ctx.getAttribute(CommonAttributes.O_DATA_ENTITY_ATTRIBUTE));
}
ResourceState state = ctx.getCurrentState();
List<Link> links = new ArrayList<Link>();
if (resourceEntity == null)
return links;
Object entity = null;
CollectionResource<?> collectionResource = null;
if (resourceEntity instanceof EntityResource) {
entity = ((EntityResource<?>) resourceEntity).getEntity();
} else if (resourceEntity instanceof CollectionResource) {
collectionResource = (CollectionResource<?>) resourceEntity;
// TODO add support for properties on collections
LOGGER.info("Injecting links into a collection, only support simple, non template, links as there are no properties on the collection at the moment");
} else if (resourceEntity instanceof MetaDataResource) {
// TODO deprecate all resource types apart from item
// (EntityResource) and collection (CollectionResource)
LOGGER.debug("Returning from the call to getLinks for a MetaDataResource without doing anything");
return links;
} else {
throw new RuntimeException("Unable to get links, an error occurred");
}
// add link to GET 'self'
if (selfTransition == null)
selfTransition = state.getSelfTransition();
LinkGenerator selfLinkGenerator = new LinkGeneratorImpl(this, selfTransition, ctx);
links.addAll(selfLinkGenerator.createLink(resourceProperties, ctx.getQueryParameters(), entity));
/*
* Add links to other application states (resources)
*/
List<Transition> transitions = state.getTransitions();
for (Transition transition : transitions) {
if (transition.getTarget() == null) {
LOGGER.warn("Skipping invalid transition: {}", transition);
continue;
}
TransitionCommandSpec cs = transition.getCommand();
if (cs.isAutoTransition()) {
// Au revoir - Auto transitions should not be seen by user
// agents
continue;
}
/*
* build link and add to list of links
*/
if (cs.isForEach() || cs.isEmbeddedForEach()) {
if (collectionResource != null) {
for (EntityResource<?> er : collectionResource.getEntities()) {
Collection<Link> eLinks = er.getLinks();
if (eLinks == null) {
eLinks = new ArrayList<Link>();
}
LinkGenerator linkGenerator = new LinkGeneratorImpl(this, transition, ctx);
Collection<Link> generatedLinks = linkGenerator.createLink(resourceProperties, ctx.getQueryParameters(), er.getEntity());
if (addLink(transition, ctx, er, rimHander)) {
eLinks.addAll(generatedLinks);
}
er.setLinks(eLinks);
if (cs.isEmbeddedForEach()) {
// Embedded resource
MultivaluedMap<String, String> newPathParameters = new MultivaluedMapImpl<String>();
newPathParameters.putAll(ctx.getPathParameters());
EntityMetadata entityMetadata = metadata.getEntityMetadata(collectionResource
.getEntityName());
List<String> ids = new ArrayList<String>();
Object tmpObj = er.getEntity();
if (tmpObj instanceof Entity) {
EntityProperty prop = ((Entity) tmpObj).getProperties().getProperty(ids.get(0));
ids.add(prop.getValue().toString());
} else if (tmpObj instanceof OEntity) {
OEntityKey entityKey = ((OEntity) tmpObj).getEntityKey();
ids.add(entityKey.toKeyStringWithoutParentheses().replaceAll("'", ""));
} else {
try {
String fieldName = entityMetadata.getIdFields().get(0);
String methodName = "get" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1);
Method method = tmpObj.getClass().getMethod(methodName);
ids.add(method.invoke(tmpObj).toString());
} catch (Exception e) {
LOGGER.warn("Failed to add record id while trying to embed current collection resource", e);
}
}
newPathParameters.put("id", ids);
InteractionContext tmpCtx = new InteractionContext(ctx, headers, newPathParameters,
ctx.getQueryParameters(), transition.getTarget());
embedResources(rimHander, headers, tmpCtx, er);
}
}
}
} else {
EntityResource<?> entityResource = null;
if (ctx.getResource() instanceof EntityResource<?>) {
entityResource = ((EntityResource<?>) ctx.getResource());
}
if (addLink(transition, ctx, entityResource, rimHander)) {
LinkGenerator linkGenerator = new LinkGeneratorImpl(this, transition, ctx);
links.addAll(linkGenerator.createLink(resourceProperties, ctx.getQueryParameters(), entity));
}
}
}
resourceEntity.setLinks(links);
return links;
}
/**
* Execute and return all the valid embedded links (target states) from the
* supplied resource. Should be identical to
* {@link InteractionContext#getResource()}.
*
* @param rimHandler
* @param headers
* @param ctx
* @param resource
* @return
*/
public Map<Transition, RESTResource> embedResources(HTTPHypermediaRIM rimHandler, HttpHeaders headers,
InteractionContext ctx, RESTResource resource) {
ResourceRequestHandler resourceRequestHandler = rimHandler.getResourceRequestHandler();
assert (resourceRequestHandler != null);
try {
ResourceRequestConfig.Builder configBuilder = new ResourceRequestConfig.Builder();
Collection<Link> links = resource.getLinks();
if (links != null) {
for (Link link : links) {
if (link == null) {
LOGGER.warn("embedResources : null Link detected.");
} else {
Transition t = link.getTransition();
/*
* when embedding resources we don't want to embed
* ourselves we only want to embed the 'EMBEDDED'
* transitions
*/
if (t.getSource() != t.getTarget()) {
if ((t.getCommand().getFlags() & Transition.EMBEDDED) == Transition.EMBEDDED) {
configBuilder.transition(t);
}
if ((t.getCommand().getFlags() & Transition.FOR_EACH_EMBEDDED) == Transition.FOR_EACH_EMBEDDED) {
configBuilder.transition(t);
}
}
}
}
}
ResourceRequestConfig config = configBuilder.build();
Map<Transition, ResourceRequestResult> results = null;
if (resource instanceof EntityResource<?>
&& resourceRequestHandler instanceof SequentialResourceRequestHandler) {
/*
* Handle cases where we may be embedding a resource that has
* filter criteria whose values are contained in the current
* resource's entity properties.
*/
Object tmpEntity = ((EntityResource) resource).getEntity();
results = ((SequentialResourceRequestHandler) resourceRequestHandler).getResources(rimHandler, headers,
ctx, null, tmpEntity, config);
} else {
results = resourceRequestHandler.getResources(rimHandler, headers, ctx, null, config);
}
if (config.getTransitions() != null && !config.getTransitions().isEmpty()
&& new HashSet(config.getTransitions()).size() != results.keySet().size()) {
throw new InteractionException(Status.INTERNAL_SERVER_ERROR, "Resource state ["
+ ctx.getCurrentState().getId() + "] did not return correct number of embedded resources.");
}
// don't replace any embedded resources added within the commands
Map<Transition, RESTResource> resourceResults = null;
if (ctx.getResource() != null && ctx.getResource().getEmbedded() != null) {
resourceResults = ctx.getResource().getEmbedded();
} else {
resourceResults = new HashMap<Transition, RESTResource>();
}
for (Transition transition : results.keySet()) {
ResourceRequestResult result = results.get(transition);
if (Family.SUCCESSFUL.equals(Status.fromStatusCode(result.getStatus()).getFamily())) {
resourceResults.put(transition, result.getResource());
} else {
LOGGER.error("Failed to embed resource for transition [{}]", transition.getId());
}
}
resource.setEmbedded(resourceResults);
return resourceResults;
} catch (InteractionException ie) {
LOGGER.error(
"Failed to embed resources [{}] with error [{} - {}]: ", ctx.getCurrentState().getId(), ie.getHttpStatus(), ie.getHttpStatus().getReasonPhrase(), ie);
throw new RuntimeException(ie);
}
}
/**
* Find the transition that was used by evaluating the LinkHeader and create
* a a Link for that transition.
*
* @param pathParameters
* @param resourceEntity
* @param linkHeader
* @return
*/
public Link getLinkFromRelations(MultivaluedMap<String, String> pathParameters, RESTResource resourceEntity,
LinkHeader linkHeader) {
Link target = null;
// Was a custom link relation supplied, informing us which link was
// used?
if (linkHeader != null) {
Set<String> relationships = linkHeader.getLinksByRelationship().keySet();
for (String related : relationships) {
Transition transition = getTransitionsById().get(related);
if (transition != null) {
LinkGenerator linkGenerator = new LinkGeneratorImpl(this, transition, null);
Collection<Link> links = linkGenerator.createLink(pathParameters, null, resourceEntity);
target = (!links.isEmpty()) ? links.iterator().next() : null;
}
}
}
return target;
}
public Map<String, Transition> getTransitionsById() {
return transitionsById;
}
public Map<String, Transition> getTransitionsByRel() {
return transitionsByRel;
}
/**
* Find the transition that was used by assuming the HTTP method was applied
* to this state; create a a Link for that transition.
*
* @param pathParameters
* @param resourceEntity
* @param currentState
* @param method
* @return
* @invariant method != null
*/
public Link getLinkFromMethod(MultivaluedMap<String, String> pathParameters, RESTResource resourceEntity,
ResourceState currentState, String method) {
assert (method != null);
Link target = null;
for (ResourceState nextState : currentState.getAllTargets()) {
Transition transition = currentState.getTransition(nextState);
if (method.contains(transition.getCommand().getMethod())) {
// do not create link if this a pseudo state, effectively no
// state
if (!transition.getTarget().isPseudoState()) {
LinkGenerator linkGenerator = new LinkGeneratorImpl(this, transition, null);
Collection<Link> links = linkGenerator.createLink(pathParameters, null, resourceEntity);
target = (!links.isEmpty()) ? links.iterator().next() : null;
}
}
}
return target;
}
public ResourceStateAndParameters resolveDynamicState(DynamicResourceState dynamicResourceState,
Map<String, Object> transitionProperties, InteractionContext ctx) {
DynamicResourceStateResolver dynamicResourceStateResolver = new DynamicResourceStateResolver(dynamicResourceState, resourceLocatorProvider);
dynamicResourceStateResolver.setParameterResolverProvider(parameterResolverProvider);
dynamicResourceStateResolver.addProperties(transitionProperties);
dynamicResourceStateResolver.addProperties(ctx.getAttributes());
ResourceStateAndParameters result = dynamicResourceStateResolver.resolve();
if (result != null) {
registerResolvedDynamicState(result.getState());
}
return result;
}
public boolean registerResolvedDynamicState(ResourceState resourceState) {
boolean registrationRequired = false;
for (Transition transition : resourceState.getTransitions()) {
ResourceState target = transition.getTarget();
if (target instanceof LazyResourceState || target instanceof LazyCollectionResourceState) {
registrationRequired = true;
}
}
if (registrationRequired) {
register(resourceState, HttpMethod.GET);
}
return registrationRequired;
}
/**
* Obtain transition properties. Transition properties are a list of entity
* properties, path parameters, and query parameters.
*
* @param transition
* transition
* @param entity
* usually an entity of the source state
* @param pathParameters
* path parameters
* @param pathParameters
* path parameters
* @return map of transition properties
*/
public Map<String, Object> getTransitionProperties(Transition transition, Object entity,
MultivaluedMap<String, String> pathParameters, MultivaluedMap<String, String> queryParameters) {
return new TransitionPropertiesBuilder(transformer)
.addPathParameters(pathParameters)
.addQueryParameters(queryParameters)
.addEntity(entity)
.addTransition(transition)
.build();
}
public InteractionCommand determinAction(String event, String path) {
return null;
}
public ResourceStateProvider getResourceStateProvider() {
return resourceStateProvider;
}
public void setResourceStateProvider(ResourceStateProvider resourceStateProvider) {
this.resourceStateProvider = resourceStateProvider;
}
public ResourceLocatorProvider getResourceLocatorProvider() {
return resourceLocatorProvider;
}
public ResourceParameterResolverProvider getParameterResolverProvider() {
return parameterResolverProvider;
}
public ResourceState checkAndResolve(ResourceState targetState) {
return new LazyResourceStateResolver(resourceStateProvider).resolve(targetState);
}
// Generated builder pattern from here
public static class Builder {
private ResourceState initial;
private ResourceState exception;
private Transformer transformer;
private CommandController commandController;
private ResourceStateProvider resourceStateProvider;
private ResourceLocatorProvider resourceLocatorProvider;
private ResourceParameterResolverProvider parameterResolverProvider;
private WorkflowCommandBuilderProvider workflowCommandBuilderProvider;
private Cache responseCache;
public Builder initial(ResourceState initial) {
this.initial = initial;
return this;
}
public Builder exception(ResourceState exception) {
this.exception = exception;
return this;
}
public Builder transformer(Transformer transformer) {
this.transformer = transformer;
return this;
}
public Builder commandController(CommandController commandController) {
this.commandController = commandController;
return this;
}
public Builder resourceStateProvider(ResourceStateProvider resourceStateProvider) {
this.resourceStateProvider = resourceStateProvider;
return this;
}
public Builder resourceLocatorProvider(ResourceLocatorProvider resourceLocatorProvider) {
this.resourceLocatorProvider = resourceLocatorProvider;
return this;
}
public Builder parameterResolverProvider(ResourceParameterResolverProvider parameterResolverProvider) {
this.parameterResolverProvider = parameterResolverProvider;
return this;
}
public Builder workflowCommandBuilderProvider(WorkflowCommandBuilderProvider workflowCommandBuilderProvider) {
this.workflowCommandBuilderProvider = workflowCommandBuilderProvider;
return this;
}
public Builder responseCache(Cache cache) {
this.responseCache = cache;
return this;
}
public ResourceStateMachine build() {
return new ResourceStateMachine(this);
}
}
private ResourceStateMachine(Builder builder) {
this.initial = builder.initial;
this.exception = builder.exception;
this.transformer = builder.transformer;
this.commandController = builder.commandController;
this.resourceStateProvider = builder.resourceStateProvider;
this.resourceLocatorProvider = builder.resourceLocatorProvider;
this.parameterResolverProvider = builder.parameterResolverProvider;
this.workflowCommandBuilderProvider = builder.workflowCommandBuilderProvider;
this.responseCache = builder.responseCache;
build();
}
private boolean addLink(Transition transition, InteractionContext ctx, EntityResource<?> er,
HTTPHypermediaRIM rimHander) {
boolean addLink = true;
// evaluate the conditional expression
Expression conditionalExp = transition.getCommand().getEvaluation();
if (conditionalExp != null) {
try {
addLink = conditionalExp.evaluate(rimHander, ctx, (er != null) ? er.clone() : null);
} catch(CloneNotSupportedException cnse){ //not thrown, but added to support clone design contract
throw new RuntimeException("Failed to clone EntityResource", cnse);
}
}
return addLink;
}
}