package com.temenos.interaction.core.hypermedia; /* * #%L * interaction-core * %% * Copyright (C) 2012 - 2013 Temenos Holdings N.V. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ResourceState implements Comparable<ResourceState> { public static final String REL_SEPARATOR = " "; private final static Logger logger = LoggerFactory.getLogger(ResourceState.class); private static final String[] DEFAULT_ITEM_RELATIONS = new String[] { "item" }; /* the parent state (same entity, pseudo state is same path) */ private final ResourceState parent; /* the name of the entity which this is a state of */ private final String entityName; /* the name for this state */ private final String name; /* the path to the create the resource which represents this state of the entity */ private final String path; /* the path parameter to use as the resource identifier */ private final String pathIdParameter; /* a state not represented by a resource, a state of the same entity (see parent) */ private final boolean pseudo; /* is an initial state */ private boolean initial; /* is an exception state */ private boolean exception; /* link relations */ private final String[] rels; /* the actions that will be executed upon viewing or entering this state */ private final List<Action> actions; /* the UriSpecification is used to append the path parameter template to the path */ private final UriSpecification uriSpecification; /* The max-age to impose on fetched entities */ private int maxAge; private List<Transition> transitions = new ArrayList<Transition>(); /* error state */ private ResourceState errorState; /** * Construct a pseudo ResourceState. A transition to one's self will not create a new resource. * @param parent * @param name */ public ResourceState(ResourceState parent, String name, List<Action> actions) { this(parent, name, actions, null); } /** * {@link ResourceState(ResourceState, String)} * @param entityName the name of the entity that this object is a state of * @param name this states name * @param path the partial URI to this state, will be prepended with supplied ResourceState path */ public ResourceState(ResourceState parent, String name, List<Action> actions, String path) { this(parent, name, actions, path, null); } public ResourceState(ResourceState parent, String name, List<Action> actions, String path, String[] rels) { this(parent, parent.getEntityName(), name, actions, parent.getPath() + (path == null ? "" : path), null, rels, path == null, null, null); } /** * Construct a substate ResourceState. A transition to a substate state will create a new resource. * @param entityName the name of the entity that this object is a state of * @param name this states name * @param path the fully qualified URI to this state */ public ResourceState(String entityName, String name, List<Action> actions, String path) { this(null, entityName, name, actions, path, null, null, false, null, null); } public ResourceState(String entityName, String name, List<Action> actions, String path, String[] rels) { this(null, entityName, name, actions, path, null, rels, false, null, null); } public ResourceState(String entityName, String name, List<Action> actions, String path, String[] rels, UriSpecification uriSpec) { this(null, entityName, name, actions, path, null, rels, false, uriSpec, null); } /** * Construct a resource state. * @param entityName Entity name * @param name state name * @param actions actions * @param path resource path * @param rels link relations * @param uriSpec uri specification * @param errorState error resource state */ public ResourceState(String entityName, String name, List<Action> actions, String path, String[] rels, UriSpecification uriSpec, ResourceState errorState) { this(null, entityName, name, actions, path, null, rels, false, uriSpec, errorState); } /** * Construct a substate ResourceState. A transition to a substate state will create a new resource. * @param entityName the name of the entity that this object is a state of * @param name this states name * @param path the uri to this state * @param pathIdParameter override the default {id} path parameter and use the value instead */ public ResourceState(String entityName, String name, List<Action> actions, String path, String pathIdParameter) { this(null, entityName, name, actions, path, pathIdParameter, null, false, null, null); } public ResourceState(String entityName, String name, List<Action> actions, String path, String pathIdParameter, String[] rels) { this(null, entityName, name, actions, path, pathIdParameter, rels, false, null, null); } /** * Construct a ResourceState. This object contains the instance information required to * create and service a resource. * @param uriSpecification the definition of the pathParameters available to a command bound to * this resource state. */ public ResourceState(String entityName, String name, List<Action> actions, String path, UriSpecification uriSpec) { this(null, entityName, name, actions, path, null, null, false, uriSpec, null); } private ResourceState(ResourceState parent, String entityName, String name, List<Action> actions, String path, String pathIdParameter, String[] rels, boolean pseudo, UriSpecification uriSpec, ResourceState errorState) { assert(name != null); assert(path != null && path.length() > 0); this.parent = parent; this.entityName = entityName; this.name = name; this.path = path; this.pathIdParameter = pathIdParameter; this.initial = false; this.exception = false; this.errorState = errorState; this.pseudo = pseudo; this.actions = actions; this.uriSpecification = uriSpec; if (rels == null) { this.rels = DEFAULT_ITEM_RELATIONS; } else { this.rels = rels; } assert(this.rels != null); } public ResourceState getParent() { return parent; } public String getEntityName() { return entityName; } public String getName() { return name; } public String getId() { return entityName + "." + name; } public String getPath() { return path; } public String getResourcePath() { if (getUriSpecification() != null) return getUriSpecification().getTemplate(); return getPath(); } public String getPathIdParameter() { return pathIdParameter; } public boolean isPseudoState() { return pseudo; } public boolean isTransientState() { return (getAllTargets().size() == 1 && getTransition(getAllTargets().iterator().next()).getCommand().isRedirectTransition()); } /** * A transient state is a resource state with a single REDIRECT transition, get the * auto {@link Transition}. * @return the auto transition for this transient state * @invariant this must be a transient state {@link ResourceState#isTransientState()} */ public Transition getRedirectTransition() { assert(isTransientState()); return getTransition(getAllTargets().iterator().next()); } public boolean isInitial() { return initial; } public void setInitial(boolean flag) { initial = flag; } public boolean isException() { return exception; } public void setException(boolean flag) { exception = flag; } public void setMaxAge(int age) { maxAge = age; } public int getMaxAge() { return maxAge; } public ResourceState getErrorState() { return errorState; } public void setErrorState(ResourceState errorState) { this.errorState = errorState; } public String getRel() { String result = null; if(rels.length == 1) { result = rels[0]; } else { StringBuilder sb = new StringBuilder(); for (String r : rels) sb.append(r).append(REL_SEPARATOR); result = sb.deleteCharAt(sb.lastIndexOf(REL_SEPARATOR)).toString(); } return result; } public String[] getRels() { return rels; } public List<Action> getActions() { return actions; } public Action getViewAction() { Action action = null; if (actions != null) { for (Action a : actions) { if (a.getType().equals(Action.TYPE.VIEW)) { action = a; } } } return action; } public UriSpecification getUriSpecification() { return uriSpecification; } /** * Return the transition to get to this state. * @return */ public Transition getSelfTransition() { Map<String, String> uriLinkageMap = new HashMap<String, String>(); String[] pathParameters = HypermediaTemplateHelper.getPathTemplateParameters(getPath()); if (pathParameters != null) { for (String param : pathParameters) { uriLinkageMap.put(param, "{"+param+"}"); } } return new Transition.Builder() .source(this) .method("GET") .uriParameters(uriLinkageMap) .target(this) .build(); } /** * A Transition from this resource state to target resource state by user agent following link. * @param transition */ public void addTransition(Transition transition) { assert(transition != null); assert(transition.getSource() == null || transition.getSource() == this); transition.setSource(this); ResourceState targetState = transition.getTarget(); assert(targetState != null); if (!(targetState instanceof LazyResourceState || targetState instanceof LazyCollectionResourceState)) { intialiseTransition(transition); } //Add the transition logger.debug("Putting transition: " + transition.getCommand() + " [" + transition + "]"); transitions.add(transition); } protected void intialiseTransition(Transition transition) { ResourceState targetState = transition.getTarget(); assert(targetState != null); TransitionCommandSpec command = transition.getCommand(); assert(command != null); String httpMethod = command.getMethod(); Map<String, String> uriLinkMap = command.getUriParameters(); int transitionFlags = command.getFlags(); if (httpMethod != null && (transitionFlags & Transition.AUTO) == Transition.AUTO) throw new IllegalArgumentException("An auto transition cannot have an HttpMethod supplied"); //Copy linkage properties to ensure they are not overwritten Map<String, String> uriLinkageMap = uriLinkMap != null ? new HashMap<String, String>(uriLinkMap) : null; //Apply link properties to action parameters applyLinkPropertiesToActionParameters(uriLinkageMap, targetState); } public void setTransitions(List<Transition> transitions) { if (transitions != null) { for (Transition t : transitions) { addTransition(t); } } } /** * Add transition to another resource interaction model. * @param httpMethod * @param resourceStateModel */ public void addTransition(String httpMethod, ResourceStateMachine resourceStateModel) { assert resourceStateModel != null; addTransition(new Transition.Builder() .method(httpMethod) .target(resourceStateModel.getInitial()) .build()); } /** * Apply link parameters to action properties. * e.g. [GETEntities filter=myfilter] where [myfilter=fld eq '{code}', code="mycode"] => [filter=fld eq '{mycode}'] * @param linkParameters link properties * @param targetState target resource state */ protected void applyLinkPropertiesToActionParameters(Map<String, String> linkParameters, ResourceState targetState) { if (linkParameters != null) { for(Action action : targetState.getActions()) { if (action.getProperties() != null) { for(Entry<Object, Object> actionParameter : action.getProperties().entrySet()) { Object paramValue = actionParameter.getValue(); //reference to link property (e.g. myfilter) or the actual link property if(paramValue != null) { //Reference to a linkage property, e.g. filter=myfilter where myfilter references myfilter=fld eq '{code}' if(paramValue instanceof String && linkParameters.containsKey(paramValue)) { actionParameter.setValue(new ActionPropertyReference((String) paramValue)); } if(actionParameter.getValue() instanceof ActionPropertyReference) { ActionPropertyReference actionRefProperty = (ActionPropertyReference) actionParameter.getValue(); String paramRefValue = linkParameters.get(actionRefProperty.getKey()); if (paramRefValue != null) { String paramRefKey = "_"; Matcher m = HypermediaTemplateHelper.TEMPLATE_PATTERN.matcher(paramRefValue); while(m.find()) { String param = m.group(1); //e.g. code if(param != null) { //replace template parameter with uri linkage properties (e.g. code => mycode if code="mycode") //paramRefValue = m.replaceAll("{" + linkParameter + "}"); //e.g. a eq {code1} && b eq {code2} paramRefKey += "_" + param; //e.g. _code1_code2 } } actionRefProperty.addProperty(paramRefKey, paramRefValue); } else { logger.error("You appear to have specified a transition to a resource that requires command parameters, but you have not specified any parameters in your trasition"); } } } } } } } } /** * Return all transitions {@link Transition}} from this state. * @return */ public List<Transition> getTransitions() { return transitions; } /** * Return all auto transitions {@link Transition}} from this state. * @return */ public List<Transition> getAutoTransitions() { List<Transition> autoTransitions = new ArrayList<>(); for (Transition transition : transitions) { if (transition.isAuto()) { autoTransitions.add(transition); } } return autoTransitions; } /** * Get the transition to the supplied target state. * @param targetState * @return */ public Transition getTransition(ResourceState targetState) { Transition foundTransition = null; for (Transition t : transitions) { ResourceState currTarget = t.getTarget(); if (currTarget != null && currTarget == targetState) { foundTransition = t; break; // We've found what we're looking for so stop searching } } return foundTransition; } /** * Get the transitions to the supplied target state. * @param targetState * @return transitions */ public List<Transition> getTransitions(ResourceState targetState) { List<Transition> transitionList = new ArrayList<Transition>(); for (Transition t : transitions) { if (t.getTarget() != null && t.getTarget() == targetState) { transitionList.add(t); } } return transitionList; } public Collection<ResourceState> getAllTargets() { List<ResourceState> result = new ArrayList<ResourceState>(); for (Transition t : transitions) { ResourceState targetState = t.getTarget(); boolean contains = false; for(ResourceState tmpState: result) { if(tmpState == targetState) { contains = true; } } if(!contains) { result.add(targetState); } } return result; } /** * A final state has no transitions. * @return */ public boolean isFinalState() { return transitions.isEmpty(); } public boolean equals(Object other) { //check for self-comparison if ( this == other ) return true; if ( !(other instanceof ResourceState) ) return false; ResourceState otherState = (ResourceState) other; return entityName.equals(otherState.entityName) && name.equals(otherState.name) && ((path == null && otherState.path == null) || (path != null && path.equals(otherState.path))) && transitions.equals(otherState.transitions); } public int hashCode() { // TODO proper implementation of hashCode, important as we intend to use the in our DSL validation return entityName.hashCode() + name.hashCode() + (path != null ? path.hashCode() : 0) + transitions.hashCode(); } public String toString() { return getId(); } @Override public int compareTo(ResourceState other) { if ( this == other ) return 0; return other.getId().compareTo(getId()); } }