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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import com.temenos.interaction.core.workflow.WorkflowCommandBuilderFactory;
import org.junit.Before;
import org.junit.Test;
import org.odata4j.core.OCollection;
import org.odata4j.core.OCollections;
import org.odata4j.core.OComplexObject;
import org.odata4j.core.OComplexObjects;
import org.odata4j.core.OEntities;
import org.odata4j.core.OEntity;
import org.odata4j.core.OProperties;
import org.odata4j.core.OProperty;
import org.odata4j.edm.EdmComplexType;
import org.odata4j.edm.EdmEntitySet;
import com.temenos.interaction.core.MultivaluedMapImpl;
import com.temenos.interaction.core.command.CommandController;
import com.temenos.interaction.core.command.InteractionCommand;
import com.temenos.interaction.core.command.InteractionCommand.Result;
import com.temenos.interaction.core.command.InteractionContext;
import com.temenos.interaction.core.command.MapBasedCommandController;
import com.temenos.interaction.core.entity.Entity;
import com.temenos.interaction.core.entity.EntityMetadata;
import com.temenos.interaction.core.entity.EntityProperties;
import com.temenos.interaction.core.entity.EntityProperty;
import com.temenos.interaction.core.entity.Metadata;
import com.temenos.interaction.core.hypermedia.Action.TYPE;
import com.temenos.interaction.core.hypermedia.expression.Expression;
import com.temenos.interaction.core.hypermedia.expression.ResourceGETExpression;
import com.temenos.interaction.core.hypermedia.expression.ResourceGETExpression.Function;
import com.temenos.interaction.core.hypermedia.expression.SimpleLogicalExpressionEvaluator;
import com.temenos.interaction.core.hypermedia.validation.HypermediaValidator;
import com.temenos.interaction.core.resource.CollectionResource;
import com.temenos.interaction.core.resource.EntityResource;
import com.temenos.interaction.core.resource.RESTResource;
import com.temenos.interaction.core.rim.HTTPHypermediaRIM;
import com.temenos.interaction.core.web.RequestContext;
public class TestResourceStateMachine {
@Before
public void setup() {
// initialise the thread local request context with requestUri and baseUri
RequestContext ctx = new RequestContext("/baseuri", "/requesturi", null);
RequestContext.setRequestContext(ctx);
}
private HTTPHypermediaRIM mockRIMHandler(ResourceStateMachine rsm) {
CommandController mockCommandController = mockCommandController();
Metadata mockMetadata = mock(Metadata.class);
when(mockMetadata.getEntityMetadata(anyString())).thenReturn(mock(EntityMetadata.class));
HTTPHypermediaRIM rimHandler = new HTTPHypermediaRIM(mockCommandController, rsm, mockMetadata);
return rimHandler;
}
/*
* Evaluate custom link relation, via the Link header. See (see rfc5988) We return a Link if the header is set and
* the @{link Transition} can be found.
*/
@SuppressWarnings("unchecked")
@Test
public void testGetLinkForCustomLinkRelation() {
ResourceState existsState = new ResourceState("toaster", "exists", new ArrayList<Action>(), "/machines/toaster");
ResourceState cookingState = new ResourceState("toaster", "cooking", new ArrayList<Action>(), "/machines/toaster/cooking");
// view the resource if the toaster is cooking (could be time remaining)
existsState.addTransition(new Transition.Builder().method("GET").target(cookingState).build());
// stop the toast cooking
cookingState.addTransition(new Transition.Builder().method("DELETE").target(existsState).build());
// the entity for linkage mapping
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
// initialise application state
ResourceStateMachine stateMachine = new ResourceStateMachine(existsState);
// mock the Link header
LinkHeader linkHeader = LinkHeader.valueOf("</path>; rel=\"toaster.cooking>DELETE>toaster.exists\"");
Link targetLink = stateMachine.getLinkFromRelations(mock(MultivaluedMap.class), testResponseEntity, linkHeader);
assertNotNull(targetLink);
assertEquals("/baseuri/machines/toaster", targetLink.getHref());
assertEquals("toaster.cooking>DELETE>toaster.exists", targetLink.getId());
}
/*
* We return a Link if a @{link Transition} for supplied method can be found.
*/
@SuppressWarnings("unchecked")
@Test
public void testGetLinkForTargetState() {
ResourceState existsState = new ResourceState("toaster", "exists", new ArrayList<Action>(), "/machines/toaster");
ResourceState cookingState = new ResourceState("toaster", "cooking", new ArrayList<Action>(), "/machines/toaster/cooking");
// view the resource if the toaster is cooking (could be time remaining)
existsState.addTransition(new Transition.Builder().method("GET").target(cookingState).build());
// stop the toast cooking
cookingState.addTransition(new Transition.Builder().method("DELETE").target(existsState).build());
// the entity for linkage mapping
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
// initialise application state
ResourceStateMachine stateMachine = new ResourceStateMachine(existsState);
Link targetLink = stateMachine.getLinkFromMethod(mock(MultivaluedMap.class), testResponseEntity, cookingState, "DELETE");
assertNotNull(targetLink);
assertEquals("/baseuri/machines/toaster", targetLink.getHref());
assertEquals("toaster.cooking>DELETE>toaster.exists", targetLink.getId());
}
/*
* We return a Link if a @{link Transition} for supplied method can be found.
*/
@SuppressWarnings("unchecked")
@Test
public void testGetLinkForSelfState() {
CollectionResourceState collectionState = new CollectionResourceState("machines", "MachineView", new ArrayList<Action>(), "/machines");
// create machines
collectionState.addTransition(new Transition.Builder().method("POST").target(collectionState).build());
// the entity for linkage mapping
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
// initialise application state
ResourceStateMachine stateMachine = new ResourceStateMachine(collectionState);
Link targetLink = stateMachine.getLinkFromMethod(mock(MultivaluedMap.class), testResponseEntity, collectionState, "POST");
assertNotNull(targetLink);
// a target link the same as our current state equates to 205 Reset Content
assertEquals("/baseuri/machines", targetLink.getHref());
// we use createSelfLink under the covers so link id looks like transition to self
assertEquals("machines.MachineView>POST>machines.MachineView", targetLink.getId());
}
/*
* We return a Link if a @{link Transition} for supplied method can be found. When the target state is a pseudo
* final state, no link will be returned.
*/
@SuppressWarnings("unchecked")
@Test
public void testGetLinkForFinalPseudoState() {
ResourceState existsState = new ResourceState("toaster", "exists", new ArrayList<Action>(), "/machines/toaster/{id}");
ResourceState deletedState = new ResourceState(existsState, "deleted", new ArrayList<Action>());
// delete the toaster
existsState.addTransition(new Transition.Builder().method("DELETE").target(deletedState).build());
// the entity for linkage mapping
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
// initialise application state
ResourceStateMachine stateMachine = new ResourceStateMachine(existsState);
Link targetLink = stateMachine.getLinkFromMethod(mock(MultivaluedMap.class), testResponseEntity, existsState, "DELETE");
// no target link equates to 204 No Content
assertNull(targetLink);
}
@Test
public void testStates() {
String ENTITY_NAME = "T24CONTRACT";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState unauthorised = new ResourceState(ENTITY_NAME, "INAU", new ArrayList<Action>(), "unauthorised/{id}");
ResourceState authorised = new ResourceState(ENTITY_NAME, "LIVE", new ArrayList<Action>(), "authorised/{id}");
ResourceState reversed = new ResourceState(ENTITY_NAME, "RNAU", new ArrayList<Action>(), "reversed/{id}");
ResourceState history = new ResourceState(ENTITY_NAME, "REVE", new ArrayList<Action>(), "history/{id}");
ResourceState end = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}");
begin.addTransition(new Transition.Builder().method("PUT").target(unauthorised).build());
unauthorised.addTransition(new Transition.Builder().method("PUT").target(unauthorised).build());
unauthorised.addTransition(new Transition.Builder().method("PUT").target(authorised).build());
unauthorised.addTransition(new Transition.Builder().method("DELETE").target(end).build());
authorised.addTransition(new Transition.Builder().method("PUT").target(history).build());
history.addTransition(new Transition.Builder().method("PUT").target(reversed).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
assertEquals(6, sm.getStates().size());
Set<ResourceState> testStates = new HashSet<ResourceState>();
testStates.add(begin);
testStates.add(unauthorised);
testStates.add(authorised);
testStates.add(reversed);
testStates.add(history);
testStates.add(end);
for (ResourceState s : testStates) {
assertTrue(sm.getStates().contains(s));
}
Metadata metadata = new Metadata("");
metadata.setEntityMetadata(new EntityMetadata("T24CONTRACT"));
System.out.println(HypermediaValidator.createValidator(sm, metadata).graph());
}
/**
* Test {@link ResourceStateMachine#getStates() should return all states.}
*/
@Test
public void testGetStates() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState end = new ResourceState(exists, "end", new ArrayList<Action>());
begin.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
Collection<ResourceState> states = sm.getStates();
assertEquals("Number of states", 3, states.size());
assertTrue(states.contains(begin));
assertTrue(states.contains(exists));
assertTrue(states.contains(end));
}
@Test
public void testGetStatesWithStateNameCollisions() {
String ENTITY_NAME_1 = "Person";
String ENTITY_NAME_2 = "Note";
ResourceState state_1 = new ResourceState(ENTITY_NAME_1, "state", new ArrayList<Action>(), "{id}");
ResourceState state_2 = new ResourceState(ENTITY_NAME_2, "state", new ArrayList<Action>(), "{id}");
state_1.addTransition(new Transition.Builder().method("PUT").target(state_2).build());
ResourceStateMachine sm = new ResourceStateMachine(state_1);
// there should only be ONE state, which one is undefined
Collection<ResourceState> states = sm.getStates();
assertEquals("Number of states", 1, states.size());
}
@Test
public void testGetTransitions() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState end = new ResourceState(exists, "end", new ArrayList<Action>());
begin.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
Map<String, Transition> transitions = sm.getTransitionsById();
assertEquals("Number of transitions", 3, transitions.size());
assertNotNull(transitions.get(".begin>PUT>.exists"));
assertNotNull(transitions.get(".exists>PUT>.exists"));
assertNotNull(transitions.get(".exists>DELETE>.end"));
}
@Test
public void testInteractionByPath() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState end = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}");
begin.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
Map<String, Set<String>> interactionMap = sm.getInteractionByPath();
assertEquals("Number of resources", 1, interactionMap.size());
Set<String> entrySet = interactionMap.keySet();
assertTrue(entrySet.contains("{id}"));
Collection<String> interactions = interactionMap.get("{id}");
assertEquals("Number of interactions", 3, interactions.size());
assertTrue(interactions.contains("GET"));
assertTrue(interactions.contains("PUT"));
assertTrue(interactions.contains("DELETE"));
}
@Test
public void testInteractionByPathSingle() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "/root");
begin.addTransition(new Transition.Builder().method("GET").target(begin).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
Map<String, Set<String>> interactionMap = sm.getInteractionByPath();
assertEquals("Number of resources", 1, interactionMap.size());
Set<String> entrySet = interactionMap.keySet();
assertTrue(entrySet.contains("/root"));
Collection<String> interactions = interactionMap.get("/root");
assertEquals("Number of interactions", 1, interactions.size());
assertTrue(interactions.contains("GET"));
}
@Test
public void testInteractionByPathPsuedo() {
String ENTITY_NAME = "";
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState end = new ResourceState(exists, "end", new ArrayList<Action>());
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(exists);
Map<String, Set<String>> interactionMap = sm.getInteractionByPath();
assertEquals("Number of resources", 1, interactionMap.size());
Set<String> entrySet = interactionMap.keySet();
assertTrue(entrySet.contains("{id}"));
Collection<String> interactions = interactionMap.get("{id}");
assertEquals("Number of interactions", 3, interactions.size());
assertTrue(interactions.contains("GET"));
assertTrue(interactions.contains("PUT"));
assertTrue(interactions.contains("DELETE"));
}
@Test
public void testInteractionByPathUriLinkage() {
String ENTITY_NAME = "";
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "/test({id})");
ResourceState end = new ResourceState(exists, "end", new ArrayList<Action>());
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("id", "entityPropertyToUse");
exists.addTransition(new Transition.Builder().method("PUT").target(exists).uriParameters(uriLinkageMap).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(exists);
Map<String, Set<String>> interactionMap = sm.getInteractionByPath();
assertEquals("Number of resources", 1, interactionMap.size());
Set<String> entrySet = interactionMap.keySet();
assertTrue(entrySet.contains("/test({id})"));
Collection<String> interactions = interactionMap.get("/test({id})");
assertEquals("Number of interactions", 3, interactions.size());
assertTrue(interactions.contains("GET"));
assertTrue(interactions.contains("PUT"));
assertTrue(interactions.contains("DELETE"));
}
@Test
public void testInteractionByPathTransient() {
String ENTITY_NAME = "";
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState deleted = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}");
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(deleted).build());
// auto transition
deleted.addTransition(new Transition.Builder().flags(Transition.AUTO).target(exists).build());
ResourceStateMachine sm = new ResourceStateMachine(exists);
Map<String, Set<String>> interactionMap = sm.getInteractionByPath();
assertEquals("Number of resources", 1, interactionMap.size());
Set<String> entrySet = interactionMap.keySet();
assertTrue(entrySet.contains("{id}"));
Collection<String> interactions = interactionMap.get("{id}");
assertEquals("Number of interactions", 3, interactions.size());
assertTrue(interactions.contains("GET"));
assertTrue(interactions.contains("PUT"));
assertTrue(interactions.contains("DELETE"));
}
@Test
public void testInteractions() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState end = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}");
begin.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
Set<String> interactions = sm.getInteractions(begin);
assertEquals("Number of interactions", 3, interactions.size());
assertTrue(interactions.contains("GET"));
assertTrue(interactions.contains("PUT"));
assertTrue(interactions.contains("DELETE"));
}
@Test
public void testSubstateInteractions() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
ResourceState published = new ResourceState(ENTITY_NAME, "published", new ArrayList<Action>(), "/published");
ResourceState publishedDeleted = new ResourceState(published, "publishedDeleted", new ArrayList<Action>());
ResourceState draft = new ResourceState(ENTITY_NAME, "draft", new ArrayList<Action>(), "/draft");
ResourceState draftDeleted = new ResourceState(draft, "draftDeleted", new ArrayList<Action>());
// create draft
initial.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// updated draft
draft.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// publish
draft.addTransition(new Transition.Builder().method("PUT").target(published).build());
// delete draft
draft.addTransition(new Transition.Builder().method("DELETE").target(draftDeleted).build());
// delete published
published.addTransition(new Transition.Builder().method("DELETE").target(publishedDeleted).build());
ResourceStateMachine sm = new ResourceStateMachine(initial);
Set<String> initialInteractions = sm.getInteractions(initial);
assertEquals("Number of interactions", 1, initialInteractions.size());
assertTrue(initialInteractions.contains("GET"));
Set<String> draftInteractions = sm.getInteractions(draft);
assertEquals("Number of interactions", 2, draftInteractions.size());
assertTrue(draftInteractions.contains("PUT"));
assertTrue(draftInteractions.contains("DELETE"));
Set<String> publishInteractions = sm.getInteractions(published);
assertEquals("Number of interactions", 2, publishInteractions.size());
assertTrue(publishInteractions.contains("PUT"));
assertTrue(publishInteractions.contains("DELETE"));
Set<String> deletedInteractions = sm.getInteractions(draftDeleted);
assertEquals("Number of interactions", 2, deletedInteractions.size());
assertTrue(deletedInteractions.contains("PUT"));
assertTrue(deletedInteractions.contains("DELETE"));
Set<String> publishedDeletedInteractions = sm.getInteractions(publishedDeleted);
assertEquals("Number of interactions", 2, publishedDeletedInteractions.size());
assertTrue(publishedDeletedInteractions.contains("PUT"));
assertTrue(publishedDeletedInteractions.contains("DELETE"));
}
@Test
public void testStateByPath() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
ResourceState published = new ResourceState(initial, "published", new ArrayList<Action>(), "/published");
ResourceState publishedDeleted = new ResourceState(published, "publishedDeleted", new ArrayList<Action>());
ResourceState draft = new ResourceState(initial, "draft", new ArrayList<Action>(), "/draft");
ResourceState draftDeleted = new ResourceState(draft, "draftDeleted", new ArrayList<Action>());
// create draft
initial.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// updated draft
draft.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// publish
draft.addTransition(new Transition.Builder().method("PUT").target(published).build());
// delete draft
draft.addTransition(new Transition.Builder().method("DELETE").target(draftDeleted).build());
// delete published
published.addTransition(new Transition.Builder().method("DELETE").target(publishedDeleted).build());
ResourceStateMachine sm = new ResourceStateMachine(initial);
Map<String, Set<ResourceState>> stateMap = sm.getResourceStatesByPath();
assertEquals("Number of states", 3, stateMap.size());
Set<String> entrySet = stateMap.keySet();
assertTrue(entrySet.contains("/entity"));
assertTrue(entrySet.contains("/entity/published"));
assertTrue(entrySet.contains("/entity/draft"));
assertEquals(1, stateMap.get("/entity").size());
assertEquals(initial, stateMap.get("/entity").iterator().next());
assertEquals(2, stateMap.get("/entity/published").size());
assertTrue(stateMap.get("/entity/published").contains(published));
assertTrue(stateMap.get("/entity/published").contains(publishedDeleted));
assertEquals(2, stateMap.get("/entity/draft").size());
assertTrue(stateMap.get("/entity/draft").contains(draft));
assertTrue(stateMap.get("/entity/draft").contains(draftDeleted));
}
@Test
public void testStateByPathSingle() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
ResourceStateMachine sm = new ResourceStateMachine(initial);
Map<String, Set<ResourceState>> stateMap = sm.getResourceStatesByPath();
assertEquals("Number of states", 1, stateMap.size());
Set<String> entrySet = stateMap.keySet();
assertTrue(entrySet.contains("/entity"));
}
@Test
public void testStateByPathPseudo() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
ResourceState draft = new ResourceState(initial, "draft", new ArrayList<Action>(), "/draft");
ResourceState deleted = new ResourceState(initial, "deleted", null);
// create draft
initial.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// updated draft
draft.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// delete draft
draft.addTransition(new Transition.Builder().method("DELETE").target(deleted).build());
// delete entity
initial.addTransition(new Transition.Builder().method("DELETE").target(deleted).build());
ResourceStateMachine sm = new ResourceStateMachine(initial);
Map<String, Set<ResourceState>> stateMap = sm.getResourceStatesByPath();
assertEquals("Number of states", 3, sm.getStates().size());
assertEquals("Number of real states", 2, stateMap.size());
Set<String> entrySet = stateMap.keySet();
assertTrue(entrySet.contains("/entity"));
assertEquals(2, stateMap.get("/entity").size());
assertTrue(stateMap.get("/entity").contains(initial));
assertTrue(stateMap.get("/entity").contains(deleted));
assertTrue(entrySet.contains("/entity/draft"));
assertEquals(1, stateMap.get("/entity/draft").size());
assertTrue(stateMap.get("/entity/draft").contains(draft));
}
@Test
public void testGetResourceStatesByPath() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
ResourceState published = new ResourceState(initial, "published", new ArrayList<Action>(), "/published");
ResourceState publishedDeleted = new ResourceState(published, "publishedDeleted", new ArrayList<Action>());
ResourceState draft = new ResourceState(initial, "draft", new ArrayList<Action>(), "/draft");
ResourceState draftDeleted = new ResourceState(draft, "draftDeleted", new ArrayList<Action>());
// create draft
initial.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// updated draft
draft.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// publish
draft.addTransition(new Transition.Builder().method("PUT").target(published).build());
// delete draft
draft.addTransition(new Transition.Builder().method("DELETE").target(draftDeleted).build());
// delete published
published.addTransition(new Transition.Builder().method("DELETE").target(publishedDeleted).build());
ResourceStateMachine sm = new ResourceStateMachine(initial);
Map<String, Set<ResourceState>> stateMap = sm.getResourceStatesByPath(draft);
assertEquals("Number of states", 3, stateMap.size());
Set<String> entrySet = stateMap.keySet();
assertTrue(entrySet.contains("/entity"));
assertTrue(entrySet.contains("/entity/published"));
assertTrue(entrySet.contains("/entity/draft"));
assertEquals(2, stateMap.get("/entity/published").size());
assertTrue(stateMap.get("/entity/published").contains(published));
assertTrue(stateMap.get("/entity/published").contains(publishedDeleted));
assertEquals(2, stateMap.get("/entity/draft").size());
assertTrue(stateMap.get("/entity/draft").contains(draft));
assertTrue(stateMap.get("/entity/draft").contains(draftDeleted));
}
@Test
public void testGetInteractionsByState() {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "notes", new ArrayList<Action>(), "/notes");
initialState.setInitial(true);
ResourceState noteState = new ResourceState(entityName, "note", new ArrayList<Action>(), "/notes('{id}')");
initialState.addTransition(new Transition.Builder().method("GET").target(noteState).build());
ResourceState noteCreateState = new ResourceState(entityName, "note_create", new ArrayList<Action>(), "/note('{id}')/create");
noteState.addTransition(new Transition.Builder().method("PUT").target(noteCreateState).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
ResourceState noteDeleteState = new ResourceState(entityName, "note_delete", new ArrayList<Action>(), "/note('{id}')/delete");
noteState.addTransition(new Transition.Builder().method("DELETE").target(noteDeleteState).build());
ResourceState noteRestoreState = new ResourceState(entityName, "note_restore", new ArrayList<Action>(), "/note('{id}')/restore");
noteDeleteState.addTransition(new Transition.Builder().method("POST").target(noteRestoreState).build());
stateMachine.register(noteDeleteState, "DELETE");
Map<String, Set<String>> interactionsMap = stateMachine.getInteractionByState();
assertEquals("Number of interactions", 5, interactionsMap.size());
// notes interactions
Set<String> methods = interactionsMap.get("notes");
assertEquals("Number of methods", 1, methods.size());
assertTrue(methods.contains("GET"));
// note interactions
methods = interactionsMap.get("note");
assertEquals("Number of methods", 1, methods.size());
assertTrue(methods.contains("GET"));
// note_new interactions
methods = interactionsMap.get("note_create");
assertEquals("Number of methods", 1, methods.size());
assertTrue(methods.contains("PUT"));
// note_delete interactions
methods = interactionsMap.get("note_delete");
assertEquals("Number of methods", 1, methods.size());
assertTrue(methods.contains("DELETE"));
}
/*
* The state machine is built with a default GET interaction for the initial state.
*/
@Test
public void testDefaultMethodForInitState() {
String entityName = "EN";
ResourceState A = new ResourceState(entityName, "A", new ArrayList<Action>(), "/A");
ResourceState B = new ResourceState(entityName, "B", new ArrayList<Action>(), "/B");
ResourceState C = new ResourceState(entityName, "C", new ArrayList<Action>(), "/C");
B.addTransition(new Transition.Builder().method("POST").target(C).build());
// initialised with state A
ResourceStateMachine smInitA = new ResourceStateMachine(A);
// only the one state is present
assertEquals("Number of states", 1, smInitA.getStates().size());
// GET interaction is registered by default
assertEquals("Number of interactions for A", 1, smInitA.getInteractions(A).size());
assertTrue("GET interactions for A", smInitA.getInteractions(A).contains("GET"));
// initialised with state B
ResourceStateMachine smInitB = new ResourceStateMachine(B);
// states B and C are present
assertEquals("Number of states", 2, smInitB.getStates().size());
// GET interaction is registered by default
assertEquals("Number of interactions for B", 1, smInitB.getInteractions(B).size());
assertTrue("GET interactions for B", smInitB.getInteractions(B).contains("GET"));
// check tha no GET interaction was added for state C
assertEquals("Number of interactions for C", 1, smInitB.getInteractions(C).size());
assertTrue("POST interactions for C", smInitB.getInteractions(C).contains("POST"));
}
/*
* Create state machine with transitions: B>POST>A; C>POST>A The pair (A,POST) should be registered only once (the
* resource state machine doesn't care from which resource it comes from).
*/
@Test
public void testRegisterDuplicateMethodForState() {
String entityName = "EN";
ResourceState A = new ResourceState(entityName, "A", new ArrayList<Action>(), "/A");
ResourceState B = new ResourceState(entityName, "B", new ArrayList<Action>(), "/B");
ResourceState C = new ResourceState(entityName, "C", new ArrayList<Action>(), "/C");
ResourceStateMachine stateMachine = new ResourceStateMachine(A);
// only the one state is present
assertEquals("Number of states", 1, stateMachine.getStates().size());
// GET interaction is registered by default
assertEquals("Number of interactions for A", 1, stateMachine.getInteractions(A).size());
B.addTransition(new Transition.Builder().method("POST").target(A).build());
C.addTransition(new Transition.Builder().method("POST").target(A).build());
// B should not be registered
assertFalse(stateMachine.getResourceStateByName().containsKey(B.getName()));
// C should not be registered
assertFalse(stateMachine.getResourceStateByName().containsKey(C.getName()));
// now register the state with the method
stateMachine.register(A, "POST");
// we still have the same number of states
assertEquals("Number of states", 1, stateMachine.getStates().size());
assertTrue(stateMachine.getResourceStateByName().containsKey(A.getName()));
// A can be reached by the default GET method and the registered POST method
assertEquals("Number of interactions for A", 2, stateMachine.getInteractions(A).size());
}
/*
* Create state machine with transitions: D>GET>B; D>GET>C; B>POST>A; C>POST>A Unregister (A,POST) and check that it
* is no longer possible to reach A with a POST method.
*/
@Test
public void testUnregisterDuplicateMethodForState() {
String entityName = "EN";
ResourceState A = new ResourceState(entityName, "A", new ArrayList<Action>(), "/A");
ResourceState B = new ResourceState(entityName, "B", new ArrayList<Action>(), "/B");
ResourceState C = new ResourceState(entityName, "C", new ArrayList<Action>(), "/C");
// we use D to have all states registered by initialising the state machine
ResourceState D = new ResourceState(entityName, "D", new ArrayList<Action>(), "/D");
B.addTransition(new Transition.Builder().method("POST").target(A).build());
C.addTransition(new Transition.Builder().method("POST").target(A).build());
D.addTransition(new Transition.Builder().method("GET").target(B).build());
D.addTransition(new Transition.Builder().method("GET").target(B).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(D);
// A can be reached by the POST method
assertEquals("Number of interactions for A", 1, stateMachine.getInteractions(A).size());
assertTrue("State A can be reached by the POST interaction", stateMachine.getInteractions(A).contains("POST"));
// now unregistered POST for A
stateMachine.unregister(A, "POST");
// state A is not present anymore
assertFalse(stateMachine.getResourceStateByName().containsKey(A.getName()));
}
@Test
public void testRegisterEmbeddedTransitions() {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "notes", new ArrayList<Action>(), "/notes");
ResourceState noteState = new ResourceState(entityName, "note", new ArrayList<Action>(), "/notes('{id}')");
initialState.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("GET").target(noteState).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertEquals("Number of interactions for noteState", 1, stateMachine.getInteractions(noteState).size());
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// add a transition for a new method without registering it
initialState.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("POST").target(noteState).build());
// all results should be the same
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertEquals("Number of interactions for noteState", 1, stateMachine.getInteractions(noteState).size());
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// now register the state with the new method
stateMachine.register(noteState, "POST");
// one interaction should have been added for noteState
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertEquals("Number of interactions for noteState", 2, stateMachine.getInteractions(noteState).size());
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// create a state with the same path as noteState
ResourceState samePathState = new ResourceState(entityName, "noteCopy", new ArrayList<Action>(), "/notes('{id}')");
samePathState.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("GET").target(noteState).build());
// no changes YET when getting the resource states by path
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// only one resource registered under the path
assertEquals("Number of states under \"/notes('{id}')\" path", 1, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
// the new state should NOT be found in the resource state machine
assertFalse(stateMachine.getResourceStateByName().containsKey(samePathState.getName()));
// now register
stateMachine.register(samePathState, "GET");
// check that a new state has been registered
assertEquals("Number of states", 3, stateMachine.getStates().size());
assertEquals("Number of interactions for noteState", 2, stateMachine.getInteractions(noteState).size());
// the path of the latest added state is the same as that of noteState
assertEquals("Number of states for noteState's path", 2, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// now two resources registered under the path
assertEquals("Number of states under \"/notes('{id}')\" path", 2, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
// check that the new state is found in the resource state machine
assertTrue(stateMachine.getResourceStateByName().containsKey(samePathState.getName()));
}
@Test
public void testRegisterRegularTransitions() {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "notes", new ArrayList<Action>(), "/notes");
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
// only the initial state is present
assertEquals("Number of states", 1, stateMachine.getStates().size());
// GET interaction is registered by default
assertEquals("Number of interactions for initialStates", 1, stateMachine.getInteractions(initialState).size());
assertEquals("Number of states for initialState's path", 1, stateMachine.getResourceStatesByPath().get(initialState.getPath()).size());
ResourceState noteState = new ResourceState(entityName, "note", new ArrayList<Action>(), "/notes('{id}')");
initialState.addTransition(new Transition.Builder().method("GET").target(noteState).build());
// noteState should not be registered
assertFalse(stateMachine.getResourceStateByName().containsKey(noteState.getName()));
// now register the state with the method
stateMachine.register(noteState, "GET");
// noteState is added
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertTrue(stateMachine.getResourceStateByName().containsKey(noteState.getName()));
// check number of interactions
assertEquals("Number of interactions for initialStates", 1, stateMachine.getInteractions(noteState).size());
assertEquals("Number of interactions for noteState", 1, stateMachine.getInteractions(noteState).size());
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
}
@Test
public void testRegisterNullState() {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "notes", new ArrayList<Action>(), "/notes");
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
// state machine should contain only one state
assertEquals("Number of states", 1, stateMachine.getStates().size());
assertTrue(stateMachine.getResourceStateByName().containsKey(initialState.getName()));
// try to register a null state
stateMachine.register(null, "GET");
// state machine should contain only one state
assertEquals("Number of states", 1, stateMachine.getStates().size());
assertTrue(stateMachine.getResourceStateByName().containsKey(initialState.getName()));
}
@Test
public void testRegisterAllStates() {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "notes", new ArrayList<Action>(), "/notes");
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
ResourceState noteState = new ResourceState(entityName, "note", new ArrayList<Action>(), "/notes('{id}')");
initialState.addTransition(new Transition.Builder().method("GET").target(noteState).build());
initialState.addTransition(new Transition.Builder().method("POST").target(noteState).build());
ResourceState samePathState = new ResourceState(entityName, "noteCopy", new ArrayList<Action>(), "/notes('{id}')");
noteState.addTransition(new Transition.Builder().method("GET").target(samePathState).build());
// only the initial state is present
assertEquals("Number of states", 1, stateMachine.getStates().size());
assertTrue(stateMachine.getResourceStateByName().containsKey(initialState.getName()));
// register all states starting with initial, BUT violating the function's precondition
stateMachine.registerAllStartingFromState(initialState, "GET");
// this shouldn't add any state as initialState/GET is already registered when the state machine is built
assertEquals("Number of states", 1, stateMachine.getStates().size());
// first we unregister the initial state
stateMachine.unregister(initialState, "GET");
// state machine should be emtpy
assertEquals("Number of states", 0, stateMachine.getStates().size());
// register all states starting with initial
stateMachine.registerAllStartingFromState(initialState, "GET");
// all states should be registered
assertEquals("Number of states", 3, stateMachine.getStates().size());
assertTrue(stateMachine.getResourceStateByName().containsKey(initialState.getName()));
assertTrue(stateMachine.getResourceStateByName().containsKey(noteState.getName()));
assertTrue(stateMachine.getResourceStateByName().containsKey(samePathState.getName()));
}
@Test
public void testRegisterAllStatesWithLoops() {
String ENTITY_NAME = "";
ResourceState A = new ResourceState(ENTITY_NAME, "A", new ArrayList<Action>(), "{id}");
ResourceState J = new ResourceState(ENTITY_NAME, "J", new ArrayList<Action>(), "{id}");
ResourceState E = new ResourceState(ENTITY_NAME, "E", new ArrayList<Action>(), "{id}");
ResourceState F = new ResourceState(ENTITY_NAME, "F", new ArrayList<Action>(), "{id}");
ResourceState K = new ResourceState(ENTITY_NAME, "K", new ArrayList<Action>(), "{id}");
ResourceState B = new ResourceState(ENTITY_NAME, "B", new ArrayList<Action>(), "{id}");
A.addTransition(new Transition.Builder().method("GET").target(E).build());
A.addTransition(new Transition.Builder().method("POST").target(J).build());
J.addTransition(new Transition.Builder().method(null).target(E).build());
E.addTransition(new Transition.Builder().method("PUT").target(F).build());
E.addTransition(new Transition.Builder().method("DELETE").target(K).build());
E.addTransition(new Transition.Builder().method("GET").target(B).build());
ResourceStateMachine sm = new ResourceStateMachine(A);
Collection<ResourceState> states = sm.getStates();
assertEquals("Number of states", 6, states.size());
}
@Test
public void testUnregister() {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "notes", new ArrayList<Action>(), "/notes");
ResourceState noteState = new ResourceState(entityName, "note", new ArrayList<Action>(), "/notes('{id}')");
initialState.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("GET").target(noteState).build());
initialState.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("POST").target(noteState).build());
ResourceState samePathState = new ResourceState(entityName, "noteCopy", new ArrayList<Action>(), "/notes('{id}')");
noteState.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("GET").target(samePathState).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
assertEquals("Number of states", 3, stateMachine.getStates().size());
assertEquals("Number of interactions for noteState", 2, stateMachine.getInteractions(noteState).size());
assertEquals("Number of states for noteState's path", 2, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// two resources registered under the path
assertEquals("Number of states under \"/notes('{id}')\" path", 2, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
// check that noteState is present in the resource state machine
assertTrue(stateMachine.getResourceStateByName().containsKey(noteState.getName()));
assertEquals("Number of interactions for initialState", 1, stateMachine.getInteractions(initialState).size());
// unregister the POST method
stateMachine.unregister(noteState, "POST");
// no changes in the number of states, since noteState still has one method registered
assertEquals("Number of states", 3, stateMachine.getStates().size());
assertEquals("Number of interactions for noteState", 1, stateMachine.getInteractions(noteState).size());
assertEquals("Number of states for noteState's path", 2, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
// two resources registered under the path
assertEquals("Number of states under \"/notes('{id}')\" path", 2, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
// check that noteState is STILL present in the resource state machine
assertTrue(stateMachine.getResourceStateByName().containsKey(noteState.getName()));
// unregister the GET method for noteState
stateMachine.unregister(noteState, "GET");
// noteState should be unregistered now
assertEquals("Number of states", 2, stateMachine.getStates().size());
// one resource registered under the path
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
assertEquals("Number of states under \"/notes('{id}')\" path", 1, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
// check that noteState is NOT present in the resource state machine
assertFalse(stateMachine.getResourceStateByName().containsKey(noteState.getName()));
// try to unregister a resource with an inexistent method
stateMachine.unregister(initialState, "POST");
// we expect no changes
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertEquals("Number of interactions for initialState", 1, stateMachine.getInteractions(initialState).size());
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
assertEquals("Number of states under \"/notes('{id}')\" path", 1, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
assertTrue(stateMachine.getResourceStateByName().containsKey(initialState.getName()));
// unregister initial state
stateMachine.unregister(initialState, "GET");
// check for changes
assertEquals("Number of states", 1, stateMachine.getStates().size());
assertEquals("Number of states for noteState's path", 1, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
assertEquals("Number of states under \"/notes('{id}')\" path", 1, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
assertFalse(stateMachine.getResourceStateByName().containsKey(initialState.getName()));
// clean-up
stateMachine.unregister(samePathState, "GET");
assertEquals("Number of states", 0, stateMachine.getStates().size());
assertEquals("Number of states for noteState's path", 0, stateMachine.getResourceStatesByPath().get(noteState.getPath()).size());
assertEquals("Number of states under \"/notes('{id}')\" path", 0, stateMachine.getResourceStatesForPath("/notes('{id}')").size());
}
/*
* Unregistering a null state shouldn't change the state of the machine
*/
@Test
public void testUnregisterNullState() {
String entityName = "EN";
ResourceState A = new ResourceState(entityName, "A", new ArrayList<Action>(), "/A");
ResourceState B = new ResourceState(entityName, "B", new ArrayList<Action>(), "/B");
A.addTransition(new Transition.Builder().method("POST").target(B).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(A);
// A can be reached by the POST method
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertEquals("Number of transitions by id", 1, stateMachine.getTransitionsById().size());
assertEquals("Number of interactions by path", 2, stateMachine.getInteractionByPath().size());
assertEquals("Number of interactions by path A", 1, stateMachine.getInteractionByPath().get(A.getPath()).size());
assertEquals("Number of interactions by path B", 1, stateMachine.getInteractionByPath().get(B.getPath()).size());
assertEquals("Number of interactions by state", 2, stateMachine.getInteractionByState().size());
assertEquals("Number of interactions for state A", 1, stateMachine.getInteractionByState().get(A.getName()).size());
assertEquals("Number of interactions for state B", 1, stateMachine.getInteractionByState().get(B.getName()).size());
// try to unregister a null state
stateMachine.unregister(null, "GET");
// no changes should be detected
assertEquals("Number of states", 2, stateMachine.getStates().size());
assertEquals("Number of transitions by id", 1, stateMachine.getTransitionsById().size());
assertEquals("Number of interactions by path", 2, stateMachine.getInteractionByPath().size());
assertEquals("Number of interactions by path A", 1, stateMachine.getInteractionByPath().get(A.getPath()).size());
assertEquals("Number of interactions by path B", 1, stateMachine.getInteractionByPath().get(B.getPath()).size());
assertEquals("Number of interactions by state", 2, stateMachine.getInteractionByState().size());
assertEquals("Number of interactions for state A", 1, stateMachine.getInteractionByState().get(A.getName()).size());
assertEquals("Number of interactions for state B", 1, stateMachine.getInteractionByState().get(B.getName()).size());
}
@Test
public void testGetResourceStatesByPathRegex() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/notes");
ResourceState notesRegex = new ResourceState(ENTITY_NAME, "notesRegex", new ArrayList<Action>(), "/notes()");
ResourceState notesEntity = new ResourceState(ENTITY_NAME, "notesEntity", new ArrayList<Action>(), "/notes({id})");
ResourceState notesEntityQuoted = new ResourceState(ENTITY_NAME, "notesEntityQuoted", new ArrayList<Action>(), "/notes('{id}')");
ResourceState notesNavProperty = new ResourceState(ENTITY_NAME, "notesNavProperty", new ArrayList<Action>(), "/notes({id})/{navproperty}");
ResourceState duffnotes = new ResourceState(ENTITY_NAME, "duffnotes", new ArrayList<Action>(), "/duff/notes");
// create transitions
initial.addTransition(new Transition.Builder().method("GET").target(notesRegex).build());
initial.addTransition(new Transition.Builder().method("GET").target(notesEntity).build());
initial.addTransition(new Transition.Builder().method("GET").target(notesEntityQuoted).build());
initial.addTransition(new Transition.Builder().method("GET").target(notesNavProperty).build());
initial.addTransition(new Transition.Builder().method("GET").target(duffnotes).build());
ResourceStateMachine sm = new ResourceStateMachine(initial);
assertEquals("Number of states: initial", 1, sm.getResourceStatesForPathRegex("/notes").size());
assertEquals("Number of states: notesRegex", 1, sm.getResourceStatesForPathRegex("/notes()").size());
assertEquals("Number of states: initial", 1, sm.getResourceStatesForPathRegex("^/notes").size());
assertEquals("Number of states: notesRegex", 1, sm.getResourceStatesForPathRegex("^/notes(\\(\\))").size());
assertEquals("Number of states: initial, notesRegex", 2, sm.getResourceStatesForPathRegex("^/notes(|\\(\\))").size());
assertEquals("Number of states: notesEntity", 1, sm.getResourceStatesForPathRegex("^/notes(|[\\(.\\)])").size());
assertEquals("Number of states: initial, notesRegex, notesEntityQuoted, and notesEntity", 4, sm.getResourceStatesForPathRegex("^/notes(|\\(.*\\))").size());
assertEquals("Number of states: initial, duffnotes", 2, sm.getResourceStatesForPathRegex(".*notes").size());
assertEquals("Number of states: all", 6, sm.getResourceStatesForPathRegex(".*notes.*").size());
}
@Test
public void testGetState() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
ResourceState published = new ResourceState(initial, "published", new ArrayList<Action>(), "/published");
ResourceState draft = new ResourceState(initial, "draft", new ArrayList<Action>(), "/draft");
ResourceState deleted = new ResourceState(initial, "deleted", new ArrayList<Action>());
// create draft
initial.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// updated draft
draft.addTransition(new Transition.Builder().method("PUT").target(draft).build());
// publish
draft.addTransition(new Transition.Builder().method("PUT").target(published).build());
// delete draft
draft.addTransition(new Transition.Builder().method("DELETE").target(deleted).build());
// delete published
published.addTransition(new Transition.Builder().method("DELETE").target(deleted).build());
ResourceStateMachine sm = new ResourceStateMachine(initial);
assertEquals(2, sm.getResourceStatesForPath(null).size());
assertTrue(sm.getResourceStatesForPath(null).contains(initial));
assertTrue(sm.getResourceStatesForPath(null).contains(deleted));
assertEquals(1, sm.getResourceStatesForPath("/entity/published").size());
assertTrue(sm.getResourceStatesForPath("/entity/published").contains(published));
assertEquals(1, sm.getResourceStatesForPath("/entity/draft").size());
assertTrue(sm.getResourceStatesForPath("/entity/draft").contains(draft));
}
@Test(expected = AssertionError.class)
public void testInteractionsInvalidState() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}");
ResourceState end = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}");
begin.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("PUT").target(exists).build());
exists.addTransition(new Transition.Builder().method("DELETE").target(end).build());
ResourceStateMachine sm = new ResourceStateMachine(begin);
ResourceState other = new ResourceState("other", "initial", new ArrayList<Action>(), "/other");
sm.getInteractions(other);
}
@Test
public void testInteractionsFromDifferentStatesWithSameMethod() {
String ENTITY_NAME = "";
ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}");
ResourceState source1 = new ResourceState(ENTITY_NAME, "source1", new ArrayList<Action>(), "{id}");
ResourceState source2 = new ResourceState(ENTITY_NAME, "source2", new ArrayList<Action>(), "{id}");
begin.addTransition(new Transition.Builder().method(null).target(source1).build());
begin.addTransition(new Transition.Builder().method(null).target(source2).build());
source1.addTransition(new Transition.Builder().method(null).target(source2).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(begin);
// all results should be the same
assertEquals("Number of states", 3, stateMachine.getStates().size());
assertEquals("Number of interactions for begin", 1, stateMachine.getInteractions(begin).size());
assertEquals("Number of interactions for source1", 1, stateMachine.getInteractions(source1).size());
assertEquals("Number of interactions for source2", 1, stateMachine.getInteractions(source2).size());
}
@Test
public void testTransitionToStateMachine() {
String PROCESS_ENTITY_NAME = "process";
String TASK_ENTITY_NAME = "task";
// process behaviour
ResourceState processes = new ResourceState(PROCESS_ENTITY_NAME, "processes", new ArrayList<Action>(), "/processes");
ResourceState newProcess = new ResourceState(PROCESS_ENTITY_NAME, "new", new ArrayList<Action>(), "/new");
// create new process
processes.addTransition(new Transition.Builder().method("POST").target(newProcess).build());
// Process states
ResourceState processInitial = new ResourceState(PROCESS_ENTITY_NAME, "initialProcess", new ArrayList<Action>(), "/processes/{id}");
ResourceState processStarted = new ResourceState(processInitial, "started", new ArrayList<Action>());
ResourceState nextTask = new ResourceState(PROCESS_ENTITY_NAME, "taskAvailable", new ArrayList<Action>(), "/nextTask");
ResourceState processCompleted = new ResourceState(processInitial, "completedProcess", new ArrayList<Action>());
// start new process
newProcess.addTransition(new Transition.Builder().method("PUT").target(processInitial).build());
processInitial.addTransition(new Transition.Builder().method("PUT").target(processStarted).build());
// do a task
processStarted.addTransition(new Transition.Builder().method("GET").target(nextTask).build());
// finish the process
processStarted.addTransition(new Transition.Builder().method("DELETE").target(processCompleted).build());
ResourceStateMachine processSM = new ResourceStateMachine(processes);
// Task states
ResourceState taskAcquired = new ResourceState(TASK_ENTITY_NAME, "acquired", new ArrayList<Action>(), "/acquired");
ResourceState taskComplete = new ResourceState(TASK_ENTITY_NAME, "complete", new ArrayList<Action>(), "/completed");
ResourceState taskAbandoned = new ResourceState(taskAcquired, "abandoned", new ArrayList<Action>());
// abandon task
taskAcquired.addTransition(new Transition.Builder().method("DELETE").target(taskAbandoned).build());
// complete task
taskAcquired.addTransition(new Transition.Builder().method("PUT").target(taskComplete).build());
ResourceStateMachine taskSM = new ResourceStateMachine(taskAcquired);
/*
* acquire task by a PUT to the initial state of the task state machine (acquired)
*/
nextTask.addTransition("PUT", taskSM);
ResourceState home = new ResourceState("", "home", new ArrayList<Action>(), "/");
home.addTransition("GET", processSM);
ResourceStateMachine serviceDocumentSM = new ResourceStateMachine(home);
Map<String, Set<String>> interactionMap = serviceDocumentSM.getInteractionByPath();
assertEquals(7, interactionMap.size());
// all target states, including states not for this entity (application states)
Collection<ResourceState> targetStates = serviceDocumentSM.getInitial().getAllTargets();
assertEquals(1, targetStates.size());
assertEquals(10, serviceDocumentSM.getStates().size());
}
@SuppressWarnings("unchecked")
private InteractionContext createMockInteractionContext(ResourceState state) {
return new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mock(MultivaluedMap.class), mock(MultivaluedMap.class), state, mock(Metadata.class));
}
/*
* Test we do not return any links if our entity is null (not found).
*/
@Test
public void testGetLinksEntityNotFound() {
ResourceState initial = new ResourceState("NOTE", "initial", new ArrayList<Action>(), "/note/{id}");
// the null entity for our test
EntityResource<Object> testResponseEntity = null;
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial);
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> links = stateMachine.injectLinks(rimHandler, createMockInteractionContext(initial), testResponseEntity, headers, metadata);
assertNotNull(links);
assertTrue(links.isEmpty());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the link to 'self'
* correctly for our test resource.
*/
@Test
public void testGetLinksSelf() {
String ENTITY_NAME = "NOTE";
String resourcePath = "/notes/new";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), resourcePath);
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial);
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> links = stateMachine.injectLinks(rimHandler, createMockInteractionContext(initial), testResponseEntity, headers, metadata);
assertNotNull(links);
assertFalse(links.isEmpty());
assertEquals(1, links.size());
Link link = (Link) links.toArray()[0];
assertEquals("self", link.getRel());
assertEquals("/baseuri/notes/new", link.getHref());
assertEquals("NOTE.initial>GET>NOTE.initial", link.getId());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the link to 'self'
* correctly for our test resource; in this self link we have used a path parameter.
*/
@SuppressWarnings("unchecked")
@Test
public void testGetLinksSelfPathParameters() {
String ENTITY_NAME = "NOTE";
String resourcePath = "/notes/{id}/reviewers";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), resourcePath);
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
/*
* Mock the path parameters with the default 'id' element
*/
MultivaluedMap<String, String> mockPathparameters = new MultivaluedMapImpl<String>();
mockPathparameters.add("id", "123");
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial);
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> links = stateMachine.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mockPathparameters, mock(MultivaluedMap.class), initial, mock(Metadata.class)), testResponseEntity, headers, metadata);
assertNotNull(links);
assertFalse(links.isEmpty());
assertEquals(1, links.size());
Link link = (Link) links.toArray()[0];
assertEquals("self", link.getRel());
assertEquals("/baseuri/notes/123/reviewers", link.getHref());
assertEquals("NOTE.initial>GET>NOTE.initial", link.getId());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the link to 'self'
* correctly for our test resource.
*/
@SuppressWarnings("unchecked")
@Test
public void testGetLinksSelfTemplate() {
String ENTITY_NAME = "NOTE";
String resourcePath = "/notes/{id}";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), resourcePath);
EntityResource<Object> testResponseEntity = new EntityResource<Object>(null);
/*
* Mock the path parameters with default 'id' path element
*/
MultivaluedMap<String, String> mockPathparameters = new MultivaluedMapImpl<String>();
mockPathparameters.add("id", "123");
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial);
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> links = stateMachine.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mockPathparameters, mock(MultivaluedMap.class), initial, mock(Metadata.class)), testResponseEntity, headers, metadata);
assertNotNull(links);
assertFalse(links.isEmpty());
assertEquals(1, links.size());
Link link = (Link) links.toArray()[0];
assertEquals("self", link.getRel());
assertEquals("/baseuri/notes/123", link.getHref());
assertEquals("NOTE.initial>GET>NOTE.initial", link.getId());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the links to other
* resource in our state machine.
*/
@Test
public void testGetLinksOtherResources() {
String rootResourcePath = "/";
ResourceState initial = new ResourceState("root", "initial", new ArrayList<Action>(), rootResourcePath);
String NOTE_ENTITY = "NOTE";
String notesResourcePath = "/notes";
CollectionResourceState notesResource = new CollectionResourceState(NOTE_ENTITY, "collection", new ArrayList<Action>(), notesResourcePath);
String PERSON_ENTITY = "PERSON";
String personResourcePath = "/persons";
CollectionResourceState personsResource = new CollectionResourceState(PERSON_ENTITY, "collection", new ArrayList<Action>(), personResourcePath);
// create the transitions (links)
initial.addTransition(new Transition.Builder().method("GET").target(notesResource).build());
initial.addTransition(new Transition.Builder().method("GET").target(personsResource).build());
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial);
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> unsortedLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(initial), new EntityResource<Object>(null), headers, metadata);
assertNotNull(unsortedLinks);
assertFalse(unsortedLinks.isEmpty());
assertEquals(3, unsortedLinks.size());
/*
* expect 3 links 'self' 'collection notes' 'colleciton persons'
*/
List<Link> links = new ArrayList<Link>(unsortedLinks);
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// notes
assertEquals("collection", links.get(0).getRel());
assertEquals("/baseuri/notes", links.get(0).getHref());
assertEquals("root.initial>GET>NOTE.collection", links.get(0).getId());
// persons
assertEquals("collection", links.get(1).getRel());
assertEquals("/baseuri/persons", links.get(1).getHref());
assertEquals("root.initial>GET>PERSON.collection", links.get(1).getId());
// service root
assertEquals("self", links.get(2).getRel());
assertEquals("/baseuri/", links.get(2).getHref());
assertEquals("root.initial>GET>root.initial", links.get(2).getId());
}
private CommandController mockCommandController() {
MapBasedCommandController cc = new MapBasedCommandController();
cc.getCommandMap().put("notfound", createCommand("Customer", new Entity("Customer", new EntityProperties()), Result.FAILURE));
cc.getCommandMap().put("found", createCommand("Customer", new Entity("Customer", new EntityProperties()), Result.SUCCESS));
return cc;
}
// create command returning the supplied entity
private InteractionCommand createCommand(final String entityName, final Entity entity, final InteractionCommand.Result result) {
InteractionCommand command = new InteractionCommand() {
public Result execute(InteractionContext ctx) {
if (entity == null) {
ctx.setResource(null);
} else {
ctx.setResource(new EntityResource<Entity>(entityName, entity));
}
return result;
}
};
return command;
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the conditional links to
* other resource in our state machine.
*/
@Test
public void testShowConditionalLinks() {
String rootResourcePath = "/bookings/{bookingId}";
ResourceState initial = new ResourceState("BOOKING", "initial", new ArrayList<Action>(), rootResourcePath);
// room reserved for the booking
ResourceState room = new ResourceState(initial, "room", new ArrayList<Action>(), "/room");
// booking cancelled
ResourceState cancelled = new ResourceState(initial, "cancelled", new ArrayList<Action>(), "/cancelled", "cancelled".split(" "));
ResourceState paid = new ResourceState(initial, "paid", new ArrayList<Action>(), "/payment", "pay".split(" "));
List<Action> mockNotFound = new ArrayList<Action>();
mockNotFound.add(new Action("notfound", TYPE.VIEW));
ResourceState pwaiting = new ResourceState(paid, "pwaiting", mockNotFound, "/pwaiting", "wait".split(" "));
ResourceState pconfirmed = new ResourceState(paid, "pconfirmed", mockNotFound, "/pconfirmed", "confirmed".split(" "));
// create transitions that indicate state
initial.addTransition(new Transition.Builder().target(room).build());
initial.addTransition(new Transition.Builder().target(cancelled).build());
initial.addTransition(new Transition.Builder().flags(Transition.AUTO).target(paid).build());
// expressions can be added to the resource with the condition or anywhere in the resource state graph
initial.addTransition(new Transition.Builder().target(pwaiting).build());
initial.addTransition(new Transition.Builder().target(pconfirmed).build());
// pseudo states that do the processing
ResourceState cancel = new ResourceState(cancelled, "psuedo_cancel", new ArrayList<Action>(), null, "cancel".split(" "));
ResourceState assignRoom = new ResourceState(room, "psuedo_assignroom", new ArrayList<Action>());
ResourceState paymentDetails = new ResourceState(paid, "psuedo_setcarddetails", new ArrayList<Action>(), null, "pay".split(" "));
Map<String, String> uriLinkageMap = new HashMap<String, String>();
int transitionFlags = 0; // regular transition
// create the transitions (links)
initial.addTransition(new Transition.Builder().method("POST").target(cancel).build());
initial.addTransition(new Transition.Builder().method("PUT").target(assignRoom).build());
List<Expression> expressions = new ArrayList<Expression>();
expressions.add(new ResourceGETExpression(pconfirmed, Function.NOT_FOUND));
expressions.add(new ResourceGETExpression(pwaiting, Function.NOT_FOUND));
initial.addTransition(new Transition.Builder().method("PUT").target(paymentDetails).uriParameters(uriLinkageMap).flags(transitionFlags).evaluation(new SimpleLogicalExpressionEvaluator(expressions)).label("Make a payment").build());
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> unsortedLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(initial), new EntityResource<Object>(new Booking("123")), headers, metadata);
assertNotNull(unsortedLinks);
assertFalse(unsortedLinks.isEmpty());
assertEquals(8, unsortedLinks.size());
/*
* expect 4 links 'self' GET room GET cancelled GET paid GET pwaiting GET pconfirmed POST cancellation PUT room
* & link to PUT pwaiting (as the booking has not been paid 'pconfirmed' and is not waiting 'pwaiting')
*/
List<Link> links = new ArrayList<Link>(unsortedLinks);
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// booking
assertEquals("self", links.get(0).getRel());
assertEquals("/baseuri/bookings/123", links.get(0).getHref());
assertEquals("BOOKING.initial>GET>BOOKING.initial", links.get(0).getId());
// cancel
assertEquals("cancel", links.get(1).getRel());
assertEquals("/baseuri/bookings/123/cancelled", links.get(1).getHref());
assertEquals("BOOKING.initial>POST>BOOKING.psuedo_cancel", links.get(1).getId());
// make payment
assertEquals("pay", links.get(2).getRel());
assertEquals("/baseuri/bookings/123/payment", links.get(2).getHref());
assertEquals("BOOKING.initial>PUT(Make a payment)>BOOKING.psuedo_setcarddetails", links.get(2).getId());
// set room
assertEquals("item", links.get(3).getRel());
assertEquals("/baseuri/bookings/123/room", links.get(3).getHref());
assertEquals("BOOKING.initial>PUT>BOOKING.psuedo_assignroom", links.get(3).getId());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the conditional links to
* other resource in our state machine; in this scenario the conditional link is only available as a transition from
* the resource with the condition
*/
@Test
public void testShowConditionalLinksTargetOnlyExistsInExpression() {
String rootResourcePath = "/bookings/{bookingId}";
ResourceState initial = new ResourceState("BOOKING", "initial", new ArrayList<Action>(), rootResourcePath);
// room reserved for the booking
ResourceState room = new ResourceState(initial, "room", new ArrayList<Action>(), "/room");
ResourceState paid = new ResourceState(initial, "paid", new ArrayList<Action>(), "/payment", "pay".split(" "));
List<Action> mockNotFound = new ArrayList<Action>();
mockNotFound.add(new Action("notfound", TYPE.VIEW));
ResourceState pwaiting = new ResourceState(paid, "pwaiting", mockNotFound, "/pwaiting", "wait".split(" "));
ResourceState pconfirmed = new ResourceState(paid, "pconfirmed", mockNotFound, "/pconfirmed", "confirmed".split(" "));
// create transitions that indicate state
initial.addTransition(new Transition.Builder().flags(Transition.AUTO).target(room).build());
// pseudo states that do the processing
ResourceState paymentDetails = new ResourceState(paid, "psuedo_setcarddetails", new ArrayList<Action>(), null, "pay".split(" "));
Map<String, String> uriLinkageMap = new HashMap<String, String>();
int transitionFlags = 0; // regular transition
List<Expression> expressions = new ArrayList<Expression>();
expressions.add(new ResourceGETExpression(pconfirmed, Function.NOT_FOUND));
expressions.add(new ResourceGETExpression(pwaiting, Function.NOT_FOUND));
room.addTransition(new Transition.Builder().method("PUT").target(paymentDetails).uriParameters(uriLinkageMap).flags(transitionFlags).evaluation(new SimpleLogicalExpressionEvaluator(expressions)).label("Make a payment").build());
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> unsortedLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(room), new EntityResource<Object>(new Booking("123")), headers, metadata);
assertNotNull(unsortedLinks);
assertFalse(unsortedLinks.isEmpty());
assertEquals(2, unsortedLinks.size());
/*
* expect 2 links 'self' (the room) & link to PUT pwaiting (as the booking has not been paid 'pconfirmed' and is
* not waiting 'pwaiting')
*/
List<Link> links = new ArrayList<Link>(unsortedLinks);
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// booking
assertEquals("</baseuri/bookings/123/room>; rel=\"self\"; title=\"room\"", links.get(0).toString());
// make a payment
assertEquals("</baseuri/bookings/123/payment>; rel=\"pay\"; title=\"Make a payment\"", links.get(1).toString());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the conditional links to
* other resource in our state machine.
*/
@Test
public void testDontShowConditionalLinks() {
String rootResourcePath = "/bookings/{bookingId}";
ResourceState initial = new ResourceState("BOOKING", "initial", new ArrayList<Action>(), rootResourcePath);
// room reserved for the booking
ResourceState room = new ResourceState(initial, "room", new ArrayList<Action>(), "/room");
// booking cancelled
ResourceState cancelled = new ResourceState(initial, "cancelled", new ArrayList<Action>(), "/cancelled", "cancel".split(" "));
ResourceState paid = new ResourceState(initial, "paid", new ArrayList<Action>(), "/payment", "pay".split(" "));
List<Action> mockNotFound = new ArrayList<Action>();
mockNotFound.add(new Action("notfound", TYPE.VIEW));
ResourceState pwaiting = new ResourceState(paid, "pwaiting", mockNotFound, "/pwaiting", "wait".split(" "));
List<Action> mockFound = new ArrayList<Action>();
mockFound.add(new Action("found", TYPE.VIEW));
ResourceState pconfirmed = new ResourceState(paid, "pconfirmed", mockFound, "/pconfirmed", "confirmed".split(" "));
// create transitions that indicate state
initial.addTransition(new Transition.Builder().target(room).build());
initial.addTransition(new Transition.Builder().target(cancelled).build());
initial.addTransition(new Transition.Builder().flags(Transition.AUTO).target(paid).build());
// TODO, expressions should also be followed in determining resource state graph
initial.addTransition(new Transition.Builder().target(pwaiting).build());
initial.addTransition(new Transition.Builder().target(pconfirmed).build());
// pseudo states that do the processing
ResourceState cancel = new ResourceState(cancelled, "psuedo_cancel", new ArrayList<Action>(), null, "cancel".split(" "));
ResourceState assignRoom = new ResourceState(room, "psuedo_assignroom", new ArrayList<Action>());
ResourceState paymentDetails = new ResourceState(paid, "psuedo_setcarddetails", new ArrayList<Action>(), null, "pay".split(" "));
Map<String, String> uriLinkageMap = new HashMap<String, String>();
int transitionFlags = 0; // regular transition
// create the transitions (links)
initial.addTransition(new Transition.Builder().method("POST").target(cancel).build());
initial.addTransition(new Transition.Builder().method("PUT").target(assignRoom).build());
/*
* In this test case we are mocking that the 'pwaiting' resource was actually found or OK, rather then NOT_FOUND
*/
List<Expression> expressions = new ArrayList<Expression>();
expressions.add(new ResourceGETExpression(pconfirmed, Function.NOT_FOUND));
expressions.add(new ResourceGETExpression(pwaiting, Function.NOT_FOUND));
initial.addTransition(new Transition.Builder().method("PUT").target(paymentDetails).uriParameters(uriLinkageMap).flags(transitionFlags).evaluation(new SimpleLogicalExpressionEvaluator(expressions)).label("Make a payment").build());
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(initial, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> unsortedLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(initial), new EntityResource<Object>(new Booking("123")), headers, metadata);
assertNotNull(unsortedLinks);
assertFalse(unsortedLinks.isEmpty());
assertEquals(7, unsortedLinks.size());
/*
* expect 3 links 'self' GET room GET cancelled GET paid GET pwaiting GET pconfirmed POST cancellation PUT room
* & DO NOT show the payment link, payment should already be confirmed
*/
List<Link> links = new ArrayList<Link>(unsortedLinks);
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// booking
assertEquals("self", links.get(0).getRel());
assertEquals("/baseuri/bookings/123", links.get(0).getHref());
assertEquals("BOOKING.initial>GET>BOOKING.initial", links.get(0).getId());
// cancel
assertEquals("cancel", links.get(1).getRel());
assertEquals("/baseuri/bookings/123/cancelled", links.get(1).getHref());
assertEquals("BOOKING.initial>POST>BOOKING.psuedo_cancel", links.get(1).getId());
// set room
assertEquals("item", links.get(2).getRel());
assertEquals("/baseuri/bookings/123/room", links.get(2).getHref());
assertEquals("BOOKING.initial>PUT>BOOKING.psuedo_assignroom", links.get(2).getId());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the links for the
* collection itself.
*/
@Test
public void testGetLinksCollection() {
String NOTE_ENTITY = "NOTE";
String notesResourcePath = "/notes";
CollectionResourceState notesResource = new CollectionResourceState(NOTE_ENTITY, "collection", new ArrayList<Action>(), notesResourcePath);
String noteItemResourcePath = "/notes/{noteId}";
ResourceState noteResource = new ResourceState(NOTE_ENTITY, "item", new ArrayList<Action>(), noteItemResourcePath, "item".split(" "));
/* create the transitions (links) */
// link to form to create new note
notesResource.addTransition(new Transition.Builder().method("POST").target(new ResourceState("stack", "new", new ArrayList<Action>(), "/notes/new", "new".split(" "))).build());
/*
* define transition to view each item of the note collection no linkage map as target URI element (self) must
* exist in source entity element (also self)
*/
Map<String, String> uriLinkageMap = new HashMap<String, String>();
notesResource.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(noteResource).uriParameters(uriLinkageMap).build());
// the items of the collection
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
entities.add(new EntityResource<Object>(createTestNote("1")));
entities.add(new EntityResource<Object>(createTestNote("2")));
entities.add(new EntityResource<Object>(createTestNote("6")));
CollectionResource<Object> testResponseEntity = new CollectionResource<Object>("notes", entities);
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(notesResource, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> unsortedLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(notesResource), testResponseEntity, headers, metadata);
assertNotNull(unsortedLinks);
assertFalse(unsortedLinks.isEmpty());
assertEquals(2, unsortedLinks.size());
/*
* expect 2 links - self and one to form to create new note
*/
List<Link> links = new ArrayList<Link>(unsortedLinks);
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// notes resource (self)
assertEquals("GET", links.get(0).getMethod());
assertEquals("self", links.get(0).getRel());
assertEquals("/baseuri/notes", links.get(0).getHref());
assertEquals("NOTE.collection>GET>NOTE.collection", links.get(0).getId());
// new notes
assertEquals("POST", links.get(1).getMethod());
assertEquals("new", links.get(1).getRel());
assertEquals("/baseuri/notes/new", links.get(1).getHref());
assertEquals("NOTE.collection>POST>stack.new", links.get(1).getId());
/* collect the links defined in each entity */
List<Link> itemLinks = new ArrayList<Link>();
for (EntityResource<Object> entity : entities) {
assertNotNull(entity.getLinks());
itemLinks.addAll(entity.getLinks());
}
/*
* expect 3 links - one to each note for 'collection notes'
*/
assertFalse(itemLinks.isEmpty());
assertEquals(3, itemLinks.size());
// sort the links so we have a predictable order for this test
Collections.sort(itemLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// link to note '1'
// TODO with better rel support we should have self and NOTE.item
// assertEquals("self NOTE.item", links.get(0).getRel());
assertEquals("item", itemLinks.get(0).getRel());
assertEquals("/baseuri/notes/1", itemLinks.get(0).getHref());
assertEquals("NOTE.collection>GET>NOTE.item", itemLinks.get(0).getId());
// link to note '2'
assertEquals("item", itemLinks.get(1).getRel());
assertEquals("/baseuri/notes/2", itemLinks.get(1).getHref());
assertEquals("NOTE.collection>GET>NOTE.item", itemLinks.get(1).getId());
// link to note '6'
assertEquals("item", itemLinks.get(2).getRel());
assertEquals("/baseuri/notes/6", itemLinks.get(2).getHref());
assertEquals("NOTE.collection>GET>NOTE.item", itemLinks.get(2).getId());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the links for items in
* the collection.
*/
@Test
public void testGetLinksCollectionItems() {
String NOTE_ENTITY = "NOTE";
String notesResourcePath = "/notes";
CollectionResourceState notesResource = new CollectionResourceState(NOTE_ENTITY, "collection", new ArrayList<Action>(), notesResourcePath);
String noteItemResourcePath = "/notes/{noteId}";
ResourceState noteResource = new ResourceState(NOTE_ENTITY, "item", new ArrayList<Action>(), noteItemResourcePath, "item".split(" "));
ResourceState noteFinalState = new ResourceState(NOTE_ENTITY, "final", new ArrayList<Action>(), noteItemResourcePath, "final".split(" "));
/* create the transitions (links) */
/*
* define transition to view each item of the note collection no linkage map as target URI element (self) must
* exist in source entity element (also self)
*/
Map<String, String> uriLinkageMap = new HashMap<String, String>();
notesResource.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(noteResource).uriParameters(uriLinkageMap).build());
notesResource.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("DELETE").target(noteFinalState).uriParameters(uriLinkageMap).build());
// the items of the collection
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
entities.add(new EntityResource<Object>(createTestNote("1")));
entities.add(new EntityResource<Object>(createTestNote("2")));
entities.add(new EntityResource<Object>(createTestNote("6")));
CollectionResource<Object> testResponseEntity = new CollectionResource<Object>("notes", entities);
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(notesResource, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> baseLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(notesResource), testResponseEntity, headers, metadata);
// just one link to self, not really testing that here
assertEquals(1, baseLinks.size());
/* collect the links defined in each entity */
List<Link> links = new ArrayList<Link>();
for (EntityResource<Object> entity : entities) {
assertNotNull(entity.getLinks());
links.addAll(entity.getLinks());
}
/*
* expect 6 links - one to self for each note for 'collection notes', one to DELETE each note
*/
assertFalse(links.isEmpty());
assertEquals(6, links.size());
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
// link to DELETE note '1'
assertEquals("final", links.get(0).getRel());
assertEquals("/baseuri/notes/1", links.get(0).getHref());
assertEquals("NOTE.collection>DELETE>NOTE.final", links.get(0).getId());
assertEquals("DELETE", links.get(0).getMethod());
// link to DELETE note '2'
assertEquals("final", links.get(1).getRel());
assertEquals("/baseuri/notes/2", links.get(1).getHref());
assertEquals("NOTE.collection>DELETE>NOTE.final", links.get(1).getId());
assertEquals("DELETE", links.get(1).getMethod());
// link to DELETE note '6'
assertEquals("final", links.get(2).getRel());
assertEquals("/baseuri/notes/6", links.get(2).getHref());
assertEquals("NOTE.collection>DELETE>NOTE.final", links.get(2).getId());
assertEquals("DELETE", links.get(0).getMethod());
// link to GET note '1'
assertEquals("item", links.get(3).getRel());
assertEquals("/baseuri/notes/1", links.get(3).getHref());
assertEquals("NOTE.collection>GET>NOTE.item", links.get(3).getId());
assertEquals("GET", links.get(3).getMethod());
// link to GET note '2'
assertEquals("item", links.get(4).getRel());
assertEquals("/baseuri/notes/2", links.get(4).getHref());
assertEquals("NOTE.collection>GET>NOTE.item", links.get(4).getId());
assertEquals("GET", links.get(4).getMethod());
// link to GET note '6'
assertEquals("item", links.get(5).getRel());
assertEquals("/baseuri/notes/6", links.get(5).getHref());
assertEquals("NOTE.collection>GET>NOTE.item", links.get(5).getId());
assertEquals("GET", links.get(5).getMethod());
}
@SuppressWarnings("unchecked")
@Test
public void testGetLinkToCollectionResource() {
ResourceState airport = new ResourceState("Airport", "airport", new ArrayList<Action>(), "/Airports('{id}')", null, new UriSpecification("airport", "/Airports('{id}')"));
CollectionResourceState flights = new CollectionResourceState("Flight", "Flights", new ArrayList<Action>(), "/Airports('{id}')/Flights", null, null);
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("filter", "arrivalAirportCode eq '{code}'");
uriLinkageMap.put("id", "{code}");
airport.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkageMap).build());
// initialise and get the application state (links)
ResourceStateMachine rsm = new ResourceStateMachine(airport, new BeanTransformer());
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl<String>();
pathParameters.add("id", "123");
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> links = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), airport, mock(Metadata.class)), new EntityResource<Object>(createAirport("123", "BA")), headers, metadata);
assertNotNull(links);
assertFalse(links.isEmpty());
assertEquals(2, links.size());
assertTrue(containsLink(links, "Airport.airport>GET>Airport.airport", "/baseuri/Airports('123')"));
assertTrue(containsLink(links, "Airport.airport>GET>Flight.Flights", "/baseuri/Airports('123')/Flights?filter=arrivalAirportCode+eq+'123'"));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testGetMultipleLinksToCollectionResourceWithTokenInQueryParams() {
ResourceState airport = new ResourceState("Airport", "airport", new ArrayList<Action>(), "/Airports('{id}')", null, new UriSpecification("airport", "/Airports('{id}')"));
CollectionResourceState flights = new CollectionResourceState("Flight", "Flights", new ArrayList<Action>(), "/Flights()", null, null);
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("$filter", "arrivalAirportCode eq '{code}'");
airport.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkageMap).build());
uriLinkageMap.put("$filter", "departureAirportCode eq '{code}'");
airport.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkageMap).build());
// initialise and get the application state (links)
ResourceStateMachine rsm = new ResourceStateMachine(airport, new BeanTransformer());
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl();
pathParameters.add("id", "123");
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> links = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), airport, mock(Metadata.class)), new EntityResource<Object>(createAirport("London Luton", "LTN")), headers, metadata);
assertNotNull(links);
assertFalse(links.isEmpty());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
assertEquals("Airport.airport>GET>Airport.airport", sortedLinks.get(0).getId());
assertEquals("/baseuri/Airports('123')", sortedLinks.get(0).getHref());
assertEquals("Airport.airport>GET>Flight.Flights", sortedLinks.get(1).getId());
assertEquals("/baseuri/Flights()?$filter=arrivalAirportCode+eq+'London+Luton'", sortedLinks.get(1).getHref());
assertEquals("Airport.airport>GET>Flight.Flights", sortedLinks.get(2).getId());
assertEquals("/baseuri/Flights()?$filter=departureAirportCode+eq+'London+Luton'", sortedLinks.get(2).getHref());
assertEquals(3, links.size());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testInjectLinksForEachToSameResourceState() {
CollectionResourceState airports = new CollectionResourceState("Airports", "airports", new ArrayList<Action>(), "/Airports()", null, null);
CollectionResourceState flights = new CollectionResourceState("Flight", "Flights", new ArrayList<Action>(), "/Flights()", null, null);
Map<String, String> uriLinkage = new HashMap<String, String>();
uriLinkage.put("$filter", "arrivalAirportCode eq '{code}'");
airports.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkage).flags(Transition.FOR_EACH).label("arrival").build());
uriLinkage.put("$filter", "departureAirportCode eq '{code}'");
airports.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkage).flags(Transition.FOR_EACH).label("departure").build());
// initialise and get the application state (links)
ResourceStateMachine rsm = new ResourceStateMachine(airports, new BeanTransformer());
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl();
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
entities.add(new EntityResource<Object>(createAirport("London Luton", "LTN")));
CollectionResource<Object> collectionResource = new CollectionResource<Object>(entities);
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> collectionLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), airports, mock(Metadata.class)), collectionResource, headers, metadata);
assertNotNull(collectionLinks);
assertFalse(collectionLinks.isEmpty());
assertEquals(1, collectionLinks.size());
Collection<Link> links = entities.get(0).getLinks();
assertNotNull(links);
assertFalse(links.isEmpty());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
assertEquals("Airports.airports>GET(arrival)>Flight.Flights", sortedLinks.get(0).getId());
assertEquals("/baseuri/Flights()?$filter=arrivalAirportCode+eq+'London+Luton'", sortedLinks.get(0).getHref());
assertEquals("Airports.airports>GET(departure)>Flight.Flights", sortedLinks.get(1).getId());
assertEquals("/baseuri/Flights()?$filter=departureAirportCode+eq+'London+Luton'", sortedLinks.get(1).getHref());
assertEquals(2, links.size());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testInjectLinksForEachToSameResourceStateUriLinkage() {
CollectionResourceState airports = new CollectionResourceState("Airports", "airports", new ArrayList<Action>(), "/Airports()", null, null);
ResourceState airport = new ResourceState("Airport", "airport", new ArrayList<Action>(), "/Airports('{id}')", null, new UriSpecification("airport", "/Airports('{id}')"));
Map<String, String> uriLinkage = new HashMap<String, String>();
// just using code because its available on our mock object, real life this is arrivalAirportCode
uriLinkage.put("id", "{code}");
airports.addTransition(new Transition.Builder().method("GET").target(airport).uriParameters(uriLinkage).flags(Transition.FOR_EACH).label("origin").build());
// just using code because its available on our mock object, real life this is departureAirportCode
uriLinkage.put("id", "{iata}");
airports.addTransition(new Transition.Builder().method("GET").target(airport).uriParameters(uriLinkage).flags(Transition.FOR_EACH).label("destination").build());
// initialise and get the application state (links)
ResourceStateMachine rsm = new ResourceStateMachine(airports, new BeanTransformer());
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl();
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
entities.add(new EntityResource<Object>(createAirport("London Luton", "LTN")));
CollectionResource<Object> collectionResource = new CollectionResource<Object>(entities);
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> collectionLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), airports, mock(Metadata.class)), collectionResource, headers, metadata);
assertNotNull(collectionLinks);
assertFalse(collectionLinks.isEmpty());
assertEquals(1, collectionLinks.size());
Collection<Link> links = entities.get(0).getLinks();
assertNotNull(links);
assertFalse(links.isEmpty());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
assertEquals("Airports.airports>GET(destination)>Airport.airport", sortedLinks.get(0).getId());
assertEquals("/baseuri/Airports('LTN')", sortedLinks.get(0).getHref());
assertEquals("Airports.airports>GET(origin)>Airport.airport", sortedLinks.get(1).getId());
assertEquals("/baseuri/Airports('London+Luton')", sortedLinks.get(1).getHref());
assertEquals(2, links.size());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testGetLinkToCollectionResourceWithReferenceToExistingQueryParam() {
ResourceState airport = new ResourceState("Airport", "airport", new ArrayList<Action>(), "/Airports('{id}')", null, new UriSpecification("airport", "/Airports('{id}')"));
CollectionResourceState flights = new CollectionResourceState("Flight", "Flights", new ArrayList<Action>(), "/Flights()", null, null);
CollectionResourceState passengers = new CollectionResourceState("Passenger", "Passengers", new ArrayList<Action>(), "/Passengers()", null, null);
// Add link to list flights
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("myfilter", "arrivalAirportCode eq '{code}'");
airport.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkageMap).build());
uriLinkageMap.put("myfilter", "departureAirportCode eq '{code}'");
airport.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkageMap).build());
// Add link to list passengers for all those flights
uriLinkageMap.clear();
uriLinkageMap.put("myfilter", "{myfilter}");
flights.addTransition(new Transition.Builder().method("GET").target(passengers).uriParameters(uriLinkageMap).build());
// initialise and get the application state (links)
ResourceStateMachine rsm = new ResourceStateMachine(airport, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
// Generate links from airport to flights
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl();
pathParameters.add("id", "123");
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> airportLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), airport, mock(Metadata.class)), new EntityResource<Object>(createAirport("London Luton", "LTN")), headers, metadata);
assertNotNull(airportLinks);
assertFalse(airportLinks.isEmpty());
assertEquals(3, airportLinks.size());
// Generate links from airport to flights
for (Link airportLink : airportLinks) {
pathParameters = new MultivaluedMapImpl();
// Obtain query parameters from link
MultivaluedMap<String, String> queryParameters = new MultivaluedMapImpl();
UriBuilder uriBuilder = UriBuilder.fromUri(airportLink.getHref());
String query = uriBuilder.build(new HashMap<String, Object>()).getQuery();
if (query != null && !query.isEmpty()) {
String[] queryParams = query.split("&");
for (String queryParam : queryParams) {
String[] keyValuePair = queryParam.split("=");
queryParameters.add(keyValuePair[0], keyValuePair[1]);
}
}
// Create links
metadata = mock(Metadata.class);
Collection<Link> flightsLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, queryParameters, flights, mock(Metadata.class)), new EntityResource<Object>(null), headers, metadata);
if (airportLink.getId().equals("Airport.airport>GET(arrivalAirportCode eq '{code}')>Flight.Flights")) {
assertTrue(containsLink(flightsLinks, "Flight.Flights>GET({myfilter})>Passenger.Passengers", "/baseuri/Passengers()?myfilter=arrivalAirportCode+eq+'London+Luton'"));
} else if (airportLink.getId().equals("Airport.airport>GET(departureAirportCode eq '{code}')>Flight.Flights")) {
assertTrue(containsLink(flightsLinks, "Flight.Flights>GET({myfilter})>Passenger.Passengers", "/baseuri/Passengers()?myfilter=departureAirportCode+eq+'London+Luton'"));
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testGetLinkWithLiteralQueryParams() {
ResourceState airport = new ResourceState("Airport", "airport", new ArrayList<Action>(), "/Airports('{id}')");
ResourceState flights = new ResourceState("Operational", "operational", new ArrayList<Action>(), "/FlightStats");
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("apikey", "Some literal value");
airport.addTransition(new Transition.Builder().method("GET").target(flights).uriParameters(uriLinkageMap).build());
// initialise and get the application state (links)
ResourceStateMachine rsm = new ResourceStateMachine(airport, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl();
pathParameters.add("id", "123");
MultivaluedMap<String, String> queryParameters = new MultivaluedMapImpl();
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
InteractionContext ctx = new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, queryParameters, airport, mock(Metadata.class));
MultivaluedMap<String, String> outQueryParameters = new MultivaluedMapImpl();
outQueryParameters.add("email", "name@test.com");
ctx.getOutQueryParameters().putAll(outQueryParameters);
Collection<Link> links = rsm.injectLinks(rimHandler, ctx, new EntityResource<Object>(createAirport("London Luton", "LTN")), headers, metadata);
assertNotNull(links);
assertFalse(links.isEmpty());
assertEquals(2, links.size());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
assertEquals("Airport.airport>GET>Airport.airport", sortedLinks.get(0).getId());
assertEquals("/baseuri/Airports('123')?email=name%40test.com", sortedLinks.get(0).getHref());
assertEquals("Airport.airport>GET>Operational.operational", sortedLinks.get(1).getId());
assertEquals("/baseuri/FlightStats?email=name%40test.com&apikey=Some+literal+value", sortedLinks.get(1).getHref());
}
/*
* We use links (hypermedia) for controlling / describing application state. Test we return the links for the
* collection itself.
*/
@Test
public void testEmbedMultipleResourcesEntityResource() {
String ENTITY = "ENTITY";
List<Action> mockActions = new ArrayList<Action>();
mockActions.add(new Action("found", Action.TYPE.VIEW, null));
ResourceState parentResource = new ResourceState(ENTITY, "parentResource", new ArrayList<Action>(), "/path");
ResourceState childResource1 = new ResourceState("PROFILE", "childResource1", mockActions, "/root/profile", "profile".split(" "));
ResourceState childResource2 = new ResourceState("PREFERENCE", "childResource2", mockActions, "/root/preferences", "preferences".split(" "));
ResourceState childResource3 = new ResourceState("POSTPREF", "postpref", mockActions, "/root/postpref", "postpref".split(" "));
/* create the transitions (links) */
parentResource.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("GET").target(childResource1).build());
parentResource.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("GET").target(childResource2).build());
parentResource.addTransition(new Transition.Builder().flags(Transition.EMBEDDED).method("POST").target(childResource3).build());
// the mock resources
EntityResource<Object> testResponseEntity = new EntityResource<Object>(ENTITY, createTestNote("rootobject"));
// initialise and get the application state (links)
ResourceStateMachine stateMachine = new ResourceStateMachine(parentResource, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
InteractionContext mockCtx = createMockInteractionContext(parentResource);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
stateMachine.injectLinks(rimHandler, mockCtx, testResponseEntity, headers, metadata);
Map<Transition, RESTResource> embeddedResources = stateMachine.embedResources(rimHandler, mock(HttpHeaders.class), mockCtx, testResponseEntity);
assertNotNull(embeddedResources);
assertFalse(embeddedResources.isEmpty());
assertEquals(3, embeddedResources.size());
/*
* expect 2 resources - profile, preferences and postpref
*/
List<RESTResource> resources = new ArrayList<RESTResource>(embeddedResources.values());
// sort the resources so we have a predictable order for this test
Collections.sort(resources, new Comparator<RESTResource>() {
@Override
public int compare(RESTResource o1, RESTResource o2) {
return o1.getEntityName().compareTo(o2.getEntityName());
}
});
// postpref resources
assertEquals("POSTPREF", resources.get(0).getEntityName());
// preferences resources
assertEquals("PREFERENCE", resources.get(1).getEntityName());
// profile resource
assertEquals("PROFILE", resources.get(2).getEntityName());
}
@Test
public void testDetermineAction() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
List<Action> actions = new ArrayList<Action>();
actions.add(new Action("GETEntities", Action.TYPE.VIEW));
ResourceState notes = new ResourceState(initial, "notes", actions, "/notes");
actions = new ArrayList<Action>();
actions.add(new Action("GETEntity", Action.TYPE.VIEW));
actions.add(new Action("CreateEntity", Action.TYPE.ENTRY));
ResourceState created = new ResourceState(initial, "created", actions, "/created");
initial.addTransition(new Transition.Builder().method("PUT").target(notes).build());
initial.addTransition(new Transition.Builder().method("POST").target(created).build());
initial.addTransition(new Transition.Builder().method("GET").target(notes).build());
initial.addTransition(new Transition.Builder().method("GET").target(created).build());
// Define resource state machine
ResourceStateMachine sm = new ResourceStateMachine(initial);
CommandController mockCommandController = mock(CommandController.class);
when(mockCommandController.fetchCommand(anyString())).thenReturn(mock(InteractionCommand.class));
sm.setCommandController(mockCommandController);
sm.setWorkflowCommandBuilderProvider(new WorkflowCommandBuilderFactory(mockCommandController));
// Ensure the correct actions are used
sm.determineAction(new Event("GET", "GET"), "/entity/notes");
verify(mockCommandController).fetchCommand("GETEntities");
reset();
sm.determineAction(new Event("GET", "GET"), "/entity/created");
verify(mockCommandController).fetchCommand("GETEntity");
reset();
sm.determineAction(new Event("POST", "POST"), "/entity/created");
verify(mockCommandController).fetchCommand("CreateEntity");
}
@Test
public void testDetermineActionWorkflow() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
List<Action> actions = new ArrayList<Action>();
actions.add(new Action("GETEntity", Action.TYPE.VIEW));
actions.add(new Action("ValidateWithSomeService", Action.TYPE.ENTRY));
actions.add(new Action("CreateEntity", Action.TYPE.ENTRY));
ResourceState created = new ResourceState(initial, "created", actions, "/created");
initial.addTransition(new Transition.Builder().method("POST").target(created).build());
// Define resource state machine
ResourceStateMachine sm = new ResourceStateMachine(initial);
CommandController mockCommandController = mock(CommandController.class);
when(mockCommandController.fetchCommand(anyString())).thenReturn(mock(InteractionCommand.class));
sm.setCommandController(mockCommandController);
sm.setWorkflowCommandBuilderProvider(new WorkflowCommandBuilderFactory(mockCommandController));
sm.determineAction(new Event("POST", "POST"), "/entity/created");
verify(mockCommandController).fetchCommand("ValidateWithSomeService");
verify(mockCommandController).fetchCommand("CreateEntity");
}
@Test
public void testDetermineState() {
String ENTITY_NAME = "";
ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entity");
List<Action> actions = new ArrayList<Action>();
actions.add(new Action("GETEntities", Action.TYPE.VIEW));
ResourceState notes = new ResourceState(initial, "notes", actions, "/notes");
actions = new ArrayList<Action>();
actions.add(new Action("GETEntity", Action.TYPE.VIEW));
actions.add(new Action("CreateEntity", Action.TYPE.ENTRY));
ResourceState created = new ResourceState(initial, "created", actions, "/created");
initial.addTransition(new Transition.Builder().method("PUT").target(notes).build());
initial.addTransition(new Transition.Builder().method("POST").target(created).build());
initial.addTransition(new Transition.Builder().method("GET").target(notes).build());
initial.addTransition(new Transition.Builder().method("GET").target(created).build());
// Define resource state machine
ResourceStateMachine sm = new ResourceStateMachine(initial);
// Ensure the correct actions are used
assertEquals("notes", sm.determineState(new Event("GET", "GET"), "/entity/notes").getName());
assertEquals("created", sm.determineState(new Event("GET", "GET"), "/entity/created").getName());
assertEquals("created", sm.determineState(new Event("POST", "POST"), "/entity/created").getName());
}
@Test
public void testGetTransitionProperties() {
// Create RSM
ResourceState existsState = new ResourceState("toaster", "exists", new ArrayList<Action>(), "/machines/toaster");
ResourceState cookingState = new ResourceState("toaster", "cooking", new ArrayList<Action>(), "/machines/toaster/cooking");
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("linkParam", "def");
existsState.addTransition(new Transition.Builder().method("GET").target(cookingState).uriParameters(uriLinkageMap).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(existsState, new EntityTransformer());
// Create entity
EntityProperties customerFields = new EntityProperties();
customerFields.setProperty(new EntityProperty("name", "Fred"));
Entity entity = new Entity("Customer", customerFields);
// Create path params
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl<String>();
pathParameters.putSingle("pathParam", "abc");
// Evaluate test
Map<String, Object> transProps = stateMachine.getTransitionProperties(existsState.getTransition(cookingState), entity, pathParameters, null);
assertEquals("abc", transProps.get("pathParam")); // Check path parameter
assertEquals("def", transProps.get("linkParam")); // Check link parameter
assertEquals("Fred", transProps.get("name")); // Check entity property
}
@Test
public void testGetTransitionPropertiesWithSameEntityProperty() {
// Create RSM
ResourceState customerState = new ResourceState("Customer", "child", new ArrayList<Action>(), "/customers/{id}");
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("id", "{parent}");
customerState.addTransition(new Transition.Builder().method("GET").target(customerState).uriParameters(uriLinkageMap).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(customerState, new EntityTransformer());
// Create entity
EntityProperties customerFields = new EntityProperties();
customerFields.setProperty(new EntityProperty("id", "100"));
customerFields.setProperty(new EntityProperty("name", "Fred"));
customerFields.setProperty(new EntityProperty("parent", "123"));
Entity entity = new Entity("Customer", customerFields);
// link parameters must take priority
Map<String, Object> transProps = stateMachine.getTransitionProperties(customerState.getTransition(customerState), entity, new MultivaluedMapImpl<String>(), null);
assertEquals("123", transProps.get("id"));
}
@Test
public void testGetPathParametersForTargetState() {
// Create RSM
ResourceState existsState = new ResourceState("toaster", "exists", new ArrayList<Action>(), "/machines/toaster");
ResourceState cookingState = new ResourceState("toaster", "cooking", new ArrayList<Action>(), "/machines/toaster/cooking({id})");
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("id", "{toasterId}");
existsState.addTransition(new Transition.Builder().method("GET").target(cookingState).uriParameters(uriLinkageMap).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(existsState, new EntityTransformer());
// Create entity
EntityProperties customerFields = new EntityProperties();
customerFields.setProperty(new EntityProperty("toasterId", "SuperToaster"));
Entity entity = new Entity("Toaster", customerFields);
// Create path params
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl<String>();
// Evaluate test
Map<String, Object> transProps = stateMachine.getTransitionProperties(existsState.getTransition(cookingState), entity, pathParameters, null);
MultivaluedMap<String, String> pathParams = HypermediaTemplateHelper.getPathParametersForTargetState(existsState.getTransition(cookingState), transProps);
assertEquals("SuperToaster", pathParams.getFirst("id"));
}
/*
* Check that multiple GET (View) actions are correctly added.
*/
@Test
public void testDetermineMultipleGetAction() {
String ENTITY_NAME = "";
List<Action> actions = new ArrayList<Action>();
// Add multiple GET actions
Action expected1 = new Action("GETEntities", Action.TYPE.VIEW);
actions.add(expected1);
Action expected2 = new Action("GETEntities", Action.TYPE.VIEW);
actions.add(expected2);
ResourceState state = new ResourceState(ENTITY_NAME, "test", actions, "test");
// Create resource state machine
ResourceStateMachine sm = new ResourceStateMachine(state);
// Ensure the correct actions are present.
List<Action> actual = sm.determineActions(new Event("GET", "GET"), state);
assertEquals(2, actual.size());
assertTrue(actual.contains(expected1));
assertTrue(actual.contains(expected2));
}
@SuppressWarnings({ "unused" })
private Object createTestNote(final String id) {
return new Object() {
final String noteId = id;
public String getNoteId() {
return noteId;
}
};
}
private Object createAirport(final String id, final String iataCode) {
return new Object() {
final String code = id;
@SuppressWarnings("unused")
public String getCode() {
return code;
}
final String iata = iataCode;
@SuppressWarnings("unused")
public String getIata() {
return iata;
}
};
}
private boolean containsLink(Collection<Link> links, String id, String href) {
for (Link l : links) {
if (l.getId().equals(id) && l.getHref().equals(href)) {
return true;
}
}
// Link not found => print debug info
System.out.println("Links with id [" + id + "] and href [" + href + "] does not exist:");
for (Link l : links) {
System.out.println(" Link: id [" + l.getId() + "], href [" + l.getHref() + "]");
}
return false;
}
/*
* Check that when there is a circular transition link registration does not go into an infinite loop.
*/
@Test
public void testCircularTransition() {
// Create a couple of states.
ResourceState initialState = new ResourceState("rubbish", "rubbish", new ArrayList<Action>(), "/rubbish");
initialState.setInitial(true);
ResourceState newState = new ResourceState("rubbish", "rubbish", new ArrayList<Action>(), "/rubbish");
// Create a circular link between the states.
Transition transition1 = new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(newState).build();
initialState.addTransition(transition1);
Transition transition2 = new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(initialState).build();
newState.addTransition(transition2);
// Create a state machine
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState);
// Try to register. In the failing condition this may take some time to
// run out of memory but in that case we are going to fail anyway. In
// the working state it will return rapidly.
try {
stateMachine.register(initialState, "GET");
} catch (StackOverflowError e) {
fail("Registration failed with stack overflow error.");
} catch (Exception e) {
fail("Registration failed with unexpected exception: " + e);
}
}
/**
* Creates a Resource State Machine for Note with one transition and Injects Links with the supplied resource entity
* name
*
* @param resourceEntityName
* @return List of sorted links
*/
private List<Link> createResourceStateMachineForNotes(String resourceEntityName) {
String entityName = "Note";
ResourceState initialState = new ResourceState(entityName, "note", new ArrayList<Action>(), "/notes({noteId})");
initialState.setInitial(true);
Map<String, String> uriLinkageMap = new HashMap<String, String>();
uriLinkageMap.put("id", "{noteId}");
ResourceState noteEditState = new ResourceState(entityName, "note_edit", new ArrayList<Action>(), "/edit");
initialState.addTransition(new Transition.Builder().target(noteEditState).uriParameters(uriLinkageMap).build());
ResourceStateMachine stateMachine = new ResourceStateMachine(initialState, new BeanTransformer());
HTTPHypermediaRIM rimHandler = mockRIMHandler(stateMachine);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
Collection<Link> unsortedLinks = stateMachine.injectLinks(rimHandler, createMockInteractionContext(initialState), new EntityResource<Object>(createTestNote(resourceEntityName)), headers, metadata);
List<Link> links = new ArrayList<Link>(unsortedLinks);
// sort the links so we have a predictable order for this test
Collections.sort(links, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getId().compareTo(o2.getId());
}
});
return links;
}
/**
* Unit test to verify processing of hypermedia links containing HTTP character entities as specified in RFC 3986.
*/
@Test
public void testGetLinksContainingReservedCharacters() {
// (":", "/", "?", "#", "[", "]", "@")
assertForUrlParamWithReservedChar("123:456", "123%3A456");
assertForUrlParamWithReservedChar("123/456", "123%2F456");
assertForUrlParamWithReservedChar("123?456", "123%3F456");
assertForUrlParamWithReservedChar("123#456", "123%23456");
assertForUrlParamWithReservedChar("123[456]", "123%5B456%5D");
assertForUrlParamWithReservedChar("123@456", "123%40456");
assertForUrlParamWithReservedChar(":1/2?3#4[5]6@", "%3A1%2F2%3F3%234%5B5%5D6%40");
// "!" / "$" / "&" / "'" / "(" / ")"
// "*" / "+" / "," / ";" / "="
assertForUrlParamWithReservedChar("123!456", "123%21456");
assertForUrlParamWithReservedChar("123$456", "123%24456");
assertForUrlParamWithReservedChar("123&456", "123%26456");
assertForUrlParamWithReservedChar("123'456", "123%27456");
assertForUrlParamWithReservedChar("123(456)", "123%28456%29");
// assertForUrlParamWithReservedChar("123*456", "123%2A456"); //not supported by URLEncoder
assertForUrlParamWithReservedChar("123+456", "123%2B456");
assertForUrlParamWithReservedChar("123,456", "123%2C456");
assertForUrlParamWithReservedChar("123;456", "123%3B456");
assertForUrlParamWithReservedChar("123=456", "123%3D456");
assertForUrlParamWithReservedChar("!1$2&3'4(5)6+7,8;9=", "%211%242%263%274%285%296%2B7%2C8%3B9%3D");
}
/**
* @param resourceEntityName
* @param expectedName
*/
private void assertForUrlParamWithReservedChar(String resourceEntityName, String expectedName) {
List<Link> links = createResourceStateMachineForNotes(resourceEntityName);
// self
assertEquals("self", links.get(0).getRel());
assertEquals("/baseuri/notes(" + expectedName + ")", links.get(0).getHref());
// item
assertEquals("item", links.get(1).getRel());
assertEquals("/baseuri/edit?id=" + expectedName, links.get(1).getHref());
}
@Test
public void testInjectLinksForEachCollectionResource() {
CollectionResourceState customerState = new CollectionResourceState("customer", "customer", new ArrayList<Action>(), "/customer()", null, null);
CollectionResourceState contactState = new CollectionResourceState("contact", "contact", new ArrayList<Action>(), "/contact()", null, null);
Map<String, String> uriLinkage = new HashMap<String, String>();
uriLinkage.put("filter", "Id eq '{Contact.Email}'");
customerState.addTransition(new Transition.Builder().method("GET").target(contactState).uriParameters(uriLinkage).flags(Transition.FOR_EACH).sourceField("AField").build());
OCollection<?> contactColl = OCollections.newBuilder(null).add(createComplexObject("Email", "johnEmailAddr", "Tel", "12345")).add(createComplexObject("Email", "smithEmailAddr", "Tel", "66778")).build();
OProperty<?> contactProp = OProperties.collection("source_Contact", null, contactColl);
List<OProperty<?>> contactPropList = new ArrayList<OProperty<?>>();
contactPropList.add(contactProp);
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
OEntity entity = OEntities.createRequest(EdmEntitySet.newBuilder().build(), contactPropList, null);
entities.add(new EntityResource<Object>(entity));
ResourceStateMachine rsm = new ResourceStateMachine(customerState, getOEntityTransformer(contactColl));
CollectionResource<Object> collectionResource = new CollectionResource<Object>(entities);
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl<String>();
@SuppressWarnings("unchecked")
Collection<Link> collectionLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), customerState, mock(Metadata.class)), collectionResource, headers, metadata);
assertNotNull(collectionLinks);
assertFalse(collectionLinks.isEmpty());
assertEquals(1, collectionLinks.size());
Collection<Link> links = entities.get(0).getLinks();
assertNotNull(links);
assertFalse(links.isEmpty());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getHref().compareTo(o2.getHref());
}
});
assertEquals("/baseuri/contact()?filter=Id+eq+'johnEmailAddr'", sortedLinks.get(0).getHref());
assertEquals("/baseuri/contact()?filter=Id+eq+'smithEmailAddr'", sortedLinks.get(1).getHref());
assertEquals(2, links.size());
}
@Test
public void testInjectLinksForEachCollectionResourceTwoLevel() {
CollectionResourceState customerState = new CollectionResourceState("customer", "customer", new ArrayList<Action>(), "/customer()", null, null);
CollectionResourceState contactState = new CollectionResourceState("contact", "contact", new ArrayList<Action>(), "/contact()", null, null);
Map<String, String> uriLinkage = new HashMap<String, String>();
uriLinkage.put("filter", "Id eq '{Contact.Address.PostCode}'");
customerState.addTransition(new Transition.Builder().method("GET").target(contactState).uriParameters(uriLinkage).flags(Transition.FOR_EACH).sourceField("AField").build());
// Inner collection
OCollection<?> postCodeColl = OCollections.newBuilder(null).add(createComplexObject("PostCode", "ABCD")).add(createComplexObject("PostCode", "EFGH")).build();
// Outer Collection
OProperty<?> addressCollProperty = OProperties.collection("Address", null, postCodeColl);
List<OProperty<?>> addressPropList = new ArrayList<OProperty<?>>();
addressPropList.add(addressCollProperty);
OComplexObject addressDetails = OComplexObjects.create(EdmComplexType.newBuilder().build(), addressPropList);
OCollection<?> addressColl = OCollections.newBuilder(null).add(addressDetails).build();
OProperty<?> contactCollectionProp = OProperties.collection("source_Contact", null, addressColl);
List<OProperty<?>> contactPropList = new ArrayList<OProperty<?>>();
contactPropList.add(contactCollectionProp);
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
entities.add(new EntityResource<Object>(OEntities.createRequest(EdmEntitySet.newBuilder().build(), contactPropList, null)));
ResourceStateMachine rsm = new ResourceStateMachine(customerState, getOEntityTransformer(addressColl));
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl<String>();
CollectionResource<Object> collectionResource = new CollectionResource<Object>(entities);
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
@SuppressWarnings("unchecked")
Collection<Link> collectionLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), customerState, mock(Metadata.class)), collectionResource, headers, metadata);
assertNotNull(collectionLinks);
assertFalse(collectionLinks.isEmpty());
assertEquals(1, collectionLinks.size());
Collection<Link> links = entities.get(0).getLinks();
assertNotNull(links);
assertFalse(links.isEmpty());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getHref().compareTo(o2.getHref());
}
});
assertEquals("/baseuri/contact()?filter=Id+eq+'ABCD'", sortedLinks.get(0).getHref());
assertEquals("/baseuri/contact()?filter=Id+eq+'EFGH'", sortedLinks.get(1).getHref());
assertEquals(2, links.size());
}
@Test
public void testInjectLinksForEachCollectionResourceMultiParams() {
CollectionResourceState customerState = new CollectionResourceState("customer", "customer", new ArrayList<Action>(), "/customer()", null, null);
CollectionResourceState contactState = new CollectionResourceState("contact", "contact", new ArrayList<Action>(), "/contact()", null, null);
Map<String, String> uriLinkage = new HashMap<String, String>();
uriLinkage.put("filter", "Name eq {personName} and Id eq '{Contact.Email}'");
customerState.addTransition(new Transition.Builder().method("GET").target(contactState).uriParameters(uriLinkage).flags(Transition.FOR_EACH).sourceField("AField").build());
OCollection<?> contactColl = OCollections.newBuilder(null).add(createComplexObject("Email", "johnEmailAddr", "Tel", "12345")).add(createComplexObject("Email", "smithEmailAddr", "Tel", "66778")).build();
OProperty<?> contactProp = OProperties.collection("source_Contact", null, contactColl);
List<OProperty<?>> contactPropList = new ArrayList<OProperty<?>>();
contactPropList.add(contactProp);
List<EntityResource<Object>> entities = new ArrayList<EntityResource<Object>>();
OEntity entity = OEntities.createRequest(EdmEntitySet.newBuilder().build(), contactPropList, null);
entities.add(new EntityResource<Object>(entity));
ResourceStateMachine rsm = new ResourceStateMachine(customerState, getOEntityTransformer(contactColl));
CollectionResource<Object> collectionResource = new CollectionResource<Object>(entities);
HTTPHypermediaRIM rimHandler = mockRIMHandler(rsm);
HttpHeaders headers = mock(HttpHeaders.class);
Metadata metadata = mock(Metadata.class);
MultivaluedMap<String, String> pathParameters = new MultivaluedMapImpl<String>();
pathParameters.add("personName", "John");
@SuppressWarnings("unchecked")
Collection<Link> collectionLinks = rsm.injectLinks(rimHandler, new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), pathParameters, mock(MultivaluedMap.class), customerState, mock(Metadata.class)), collectionResource, headers, metadata);
assertNotNull(collectionLinks);
assertFalse(collectionLinks.isEmpty());
assertEquals(1, collectionLinks.size());
Collection<Link> links = entities.get(0).getLinks();
assertNotNull(links);
assertFalse(links.isEmpty());
// sort the links so we have a predictable order for this test
List<Link> sortedLinks = new ArrayList<Link>();
sortedLinks.addAll(links);
Collections.sort(sortedLinks, new Comparator<Link>() {
@Override
public int compare(Link o1, Link o2) {
return o1.getHref().compareTo(o2.getHref());
}
});
assertEquals("/baseuri/contact()?filter=Name+eq+John+and+Id+eq+'johnEmailAddr'", sortedLinks.get(0).getHref());
assertEquals("/baseuri/contact()?filter=Name+eq+John+and+Id+eq+'smithEmailAddr'", sortedLinks.get(1).getHref());
assertEquals(2, links.size());
}
private OComplexObject createComplexObject(String... values) {
List<OProperty<?>> propertyList = new ArrayList<OProperty<?>>();
for (int i = 0; i < values.length; i += 2) {
OProperty<String> property = OProperties.string(values[i], values[i + 1]);
propertyList.add(property);
}
OComplexObject complexObj = OComplexObjects.create(EdmComplexType.newBuilder().build(), propertyList);
return complexObj;
}
private Transformer getOEntityTransformer(OCollection<?> collection) {
Map<String, Object> entityProperties = new HashMap<String, Object>();
entityProperties.put("source_Contact", collection);
Transformer transformerMock = mock(Transformer.class);
when(transformerMock.transform(anyObject())).thenReturn(entityProperties);
return transformerMock;
}
@Test
public void testResolveDynamicResourceSingleParam() {
ResourceStateMachine rsm = createResourceStateMachineForResolveDynamicResource();
String[] resourceArgs = new String[] { "{Field}" };
DynamicResourceState resourceState = new DynamicResourceState("", "", "", resourceArgs);
Map<String, Object> transitionProperties = new HashMap<String, Object>();
transitionProperties.put("Field", "AVersion");
ResourceStateAndParameters result = rsm.resolveDynamicState(resourceState, transitionProperties, createMockInteractionContext(resourceState));
assertEquals("AVersion", result.getParams()[0].getValue());
}
@Test
public void testResolvedDynamicResourceCollectionParam() {
ResourceStateMachine rsm = createResourceStateMachineForResolveDynamicResource();
String[] resourceArgs = new String[] { "{Parent.Field}" };
DynamicResourceState resourceState = new DynamicResourceState("", "", "", resourceArgs);
Map<String, Object> transitionProperties = new HashMap<String, Object>();
transitionProperties.put("Parent.Field", "value1");
ResourceStateAndParameters result = rsm.resolveDynamicState(resourceState, transitionProperties, createMockInteractionContext(resourceState));
assertEquals("value1", result.getParams()[0].getValue());
}
@Test
public void testResolvedDynamicResourceMultipleParam() {
ResourceStateMachine rsm = createResourceStateMachineForResolveDynamicResource();
String[] resourceArgs = new String[] { "{Field}", "I", "{Product}" };
DynamicResourceState resourceState = new DynamicResourceState("", "", "", resourceArgs);
Map<String, Object> transitionProperties = new HashMap<String, Object>();
transitionProperties.put("Field", "AVersion");
transitionProperties.put("Product", "Account");
ResourceStateAndParameters result = rsm.resolveDynamicState(resourceState, transitionProperties, createMockInteractionContext(resourceState));
assertEquals("AVersion", result.getParams()[0].getValue());
assertEquals("I", result.getParams()[1].getValue());
assertEquals("Account", result.getParams()[2].getValue());
}
@Test
public void testResolvedDynamicResourceCollectionwithMultipleParam() {
ResourceStateMachine rsm = createResourceStateMachineForResolveDynamicResource();
String[] resourceArgs = new String[] { "{Field}", "I", "{Parent.Product}" };
DynamicResourceState resourceState = new DynamicResourceState("", "", "", resourceArgs);
Map<String, Object> transitionProperties = new HashMap<String, Object>();
transitionProperties.put("Field", "AVersion");
transitionProperties.put("Parent.Product", "Account");
ResourceStateAndParameters result = rsm.resolveDynamicState(resourceState, transitionProperties, createMockInteractionContext(resourceState));
assertEquals("AVersion", result.getParams()[0].getValue());
assertEquals("I", result.getParams()[1].getValue());
assertEquals("Account", result.getParams()[2].getValue());
}
@Test
public void testResolvedDynamicResourceCollectionwithMultipleParamFailure() {
ResourceStateMachine rsm = createResourceStateMachineForResolveDynamicResource();
String[] resourceArgs = new String[] { "{Parent1.Field}", "I", "{Parent2.Product}" };
DynamicResourceState resourceState = new DynamicResourceState("", "", "", resourceArgs);
Map<String, Object> transitionProperties = new HashMap<String, Object>();
transitionProperties.put("Parent1.Field", "AVersion");
transitionProperties.put("Parent.Product", "Account");
ResourceStateAndParameters result = rsm.resolveDynamicState(resourceState, transitionProperties, createMockInteractionContext(resourceState));
assertEquals("AVersion", result.getParams()[0].getValue());
assertEquals("I", result.getParams()[1].getValue());
assertEquals("Parent2.Product", result.getParams()[2].getValue());
}
private ResourceStateMachine createResourceStateMachineForResolveDynamicResource() {
ResourceParameterResolver parameterResolver = new ResourceParameterResolver() {
@Override
public ParameterAndValue[] resolve(Object[] aliases, ResourceParameterResolverContext context) {
ParameterAndValue[] params = new ParameterAndValue[aliases.length];
for (int i = 0; i < aliases.length; i++) {
String value = aliases[i].toString();
params[i] = new ParameterAndValue(value, value);
}
return params;
}
};
ResourceLocator resourceLocator = new ResourceLocator() {
@Override
public ResourceState resolve(Object... alias) {
return new ResourceState("entityName", "name", new ArrayList<Action>(), "path");
}};
ResourceLocatorProvider resourceLocatorProviderMock = mock(ResourceLocatorProvider.class);
when(resourceLocatorProviderMock.get(anyString())).thenReturn(resourceLocator);
ResourceParameterResolverProvider parameterResolverMock = mock(ResourceParameterResolverProvider.class);
when(parameterResolverMock.get(anyString())).thenReturn(parameterResolver);
ResourceState resState = new ResourceState("entityName", "name", new ArrayList<Action>(), "path");
ResourceStateMachine rsm = new ResourceStateMachine(resState, resourceLocatorProviderMock);
rsm.setParameterResolverProvider(parameterResolverMock);
return rsm;
}
}