package com.temenos.interaction.core.hypermedia.validation; /* * #%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.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.temenos.interaction.core.command.CommandController; import com.temenos.interaction.core.command.InteractionCommand; import com.temenos.interaction.core.entity.Metadata; import com.temenos.interaction.core.hypermedia.Action; import com.temenos.interaction.core.hypermedia.LazyCollectionResourceState; import com.temenos.interaction.core.hypermedia.LazyResourceState; import com.temenos.interaction.core.hypermedia.ResourceState; import com.temenos.interaction.core.hypermedia.ResourceStateMachine; import com.temenos.interaction.core.hypermedia.Transition; public class HypermediaValidator { private final static Logger logger = LoggerFactory.getLogger(HypermediaValidator.class); private final static String FINAL_STATE = "final"; private ResourceStateMachine hypermediaEngine; private Metadata metadata; private LogicalConfigurationListener logicalConfigurationListener; protected HypermediaValidator(ResourceStateMachine rsm, Metadata metadata) { this.hypermediaEngine = rsm; this.metadata = metadata; } public static HypermediaValidator createValidator(ResourceStateMachine rsm, Metadata metadata) { return new HypermediaValidator(rsm, metadata); } public void setLogicalConfigurationListener(LogicalConfigurationListener lcl) { this.logicalConfigurationListener = lcl; } /* * @precondition ResourceStateMachine must have had a CommandController set. */ public boolean validate() { boolean valid = true; /* * Validate the resource by attempting to fetch a command for all the required * actions for the resource state. */ for (ResourceState currentState : hypermediaEngine.getStates()) { if (currentState instanceof LazyResourceState || currentState instanceof LazyCollectionResourceState) { logger.debug("Skipping check for lazy resource state [" + currentState + "] " + currentState.getPath()); continue; } else { logger.debug("Checking configuration for [" + currentState + "] " + currentState.getPath()); } if (metadata.getEntityMetadata(currentState.getEntityName()) == null) { fireNoMetadataFound(hypermediaEngine, currentState); continue; } List<Action> actions = currentState.getActions(); if (actions == null) { fireNoActionsConfigured(hypermediaEngine, currentState); valid = false; continue; } boolean viewActionSeen = false; for (Action action : actions) { CommandController commandController = hypermediaEngine.getCommandController(); assert(commandController != null); InteractionCommand command = commandController.fetchCommand(action.getName()); if (command == null) { fireActionNotAvailable(hypermediaEngine, currentState, action); valid = false; } // TODO refine this validation to view action for regular resource state; entry action for pseudo state // if (action.getType().equals(Action.TYPE.VIEW)) { if (action.getType().equals(Action.TYPE.VIEW) || action.getType().equals(Action.TYPE.ENTRY)) { viewActionSeen = true; } } // every resource MUST have a GET command if (!viewActionSeen) { fireViewActionNotSeen(hypermediaEngine, currentState); valid = false; } } return valid; } private void fireNoMetadataFound(ResourceStateMachine rsm, ResourceState state) { if (logicalConfigurationListener != null) logicalConfigurationListener.noMetadataFound(hypermediaEngine, state); } private void fireNoActionsConfigured(ResourceStateMachine rsm, ResourceState state) { if (logicalConfigurationListener != null) logicalConfigurationListener.noActionsConfigured(hypermediaEngine, state); } private void fireActionNotAvailable(ResourceStateMachine rsm, ResourceState state, Action action) { if (logicalConfigurationListener != null) logicalConfigurationListener.actionNotAvailable(hypermediaEngine, state, action); } private void fireViewActionNotSeen(ResourceStateMachine rsm, ResourceState state) { if (logicalConfigurationListener != null) logicalConfigurationListener.viewActionNotSeen(hypermediaEngine, state); } public Set<ResourceState> unreachableStates(final Set<ResourceState> states, final ResourceStateMachine sm) { Set<ResourceState> copyStates = new HashSet<ResourceState>(states); copyStates.removeAll(sm.getStates()); return copyStates; } public boolean validate(Set<ResourceState> states, ResourceStateMachine sm) { // validate the StateMachine can reach all the defined supplied states return unreachableStates(states, sm).size() == 0; } /** * Produce a pretty DOT graph * @param sm * @return */ public String graph() { ResourceStateMachine sm = hypermediaEngine; List<ResourceState> states = new ArrayList<ResourceState>(sm.getStates()); // sort the states for a predictable output Collections.sort(states); StringBuffer sb = new StringBuffer(); sb.append("digraph ").append(sm.getInitial().getEntityName()).append(" {\n"); // declare the initial state circle sb.append(" ").append(sm.getInitial().getEntityName() + sm.getInitial().getName()).append("[shape=circle, width=.25, label=\"\", color=black, style=filled]").append("\n"); // declare the exception state ResourceState exceptionState = sm.getException(); if (exceptionState != null) { sb.append(" ").append(exceptionState.getEntityName() + exceptionState.getName()).append("[label=\"" + exceptionState.getId() + "\"]").append("\n"); } // declare all the states and set a label for (ResourceState s : states) { if (!s.equals(sm.getInitial()) && !s.isException()) { sb.append(" ").append(s.getEntityName() + s.getName()).append("[label=\"" + s.getId() + (s.getPath().length() > 0 ? (" " + s.getPath()) : "") + "\"]").append("\n"); } } int countFinal = 0; for (ResourceState s : states) { for (ResourceState targetState : s.getAllTargets()) { List<Transition> transitions = s.getTransitions(targetState); for(Transition transition : transitions) { sb.append(" ").append(s.getEntityName() + s.getName()).append("->").append(targetState.getEntityName() + targetState.getName()) .append("["); if (transition.getCommand().isAutoTransition()) { // this is an auto transition sb.append("style=\"dotted\""); } else { sb.append("label=\"") .append(transition.getCommand() + " " + transition.getTarget().getPath()) .append("\""); } sb.append("]").append("\n"); } } if (s.isFinalState()) { sb.append(" ").append(FINAL_STATE); if (countFinal > 0) { sb.append(countFinal); } sb.append("[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]").append("\n"); sb.append(" ").append(s.getEntityName() + s.getName()).append("->").append(FINAL_STATE); if (countFinal > 0) { sb.append(countFinal); } sb.append("[label=\"\"]").append("\n"); countFinal++; } } sb.append("}"); return sb.toString(); } /** * Produce a pretty DOT graph, only for the states of this current entity. * @param sm * @return */ public String graphEntityNextStates() { ResourceStateMachine sm = hypermediaEngine; Collection<ResourceState> states = sm.getStates(); StringBuffer sb = new StringBuffer(); sb.append("digraph ").append(sm.getInitial().getEntityName()).append(" {\n"); // declare the initial state circle sb.append(" ").append(sm.getInitial().getEntityName() + sm.getInitial().getName()).append("[shape=circle, width=.25, label=\"\", color=black, style=filled]").append("\n"); // declare all the state machines as a square, and set the label for states of this entity for (ResourceState s : states) { if (!s.equals(sm.getInitial())) { if (s.getEntityName().equals(sm.getInitial().getEntityName())) { sb.append(" ").append(s.getEntityName() + s.getName()).append("[label=\"" + s.getId() + "\"]").append("\n"); } else if (s.isInitial()) { sb.append(" ").append(s.getEntityName() + s.getName()).append("[shape=square, width=.25, label=\"" + s.getId() + "\"]").append("\n"); } } } int countFinal = 0; for (ResourceState s : states) { // only show transition for states of this state machine if (s.getEntityName().equals(sm.getInitial().getEntityName())) { for (ResourceState targetState : s.getAllTargets()) { List<Transition> transitions = s.getTransitions(targetState); for(Transition transition : transitions) { sb.append(" ").append(s.getEntityName() + s.getName()).append("->").append(targetState.getEntityName() + targetState.getName()) .append("[label=\"") .append(transition.getCommand() + " " + transition.getTarget().getPath()) .append("\"]").append("\n"); } } if (s.isFinalState()) { sb.append(" ").append(FINAL_STATE); if (countFinal > 0) { sb.append(countFinal); } sb.append("[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]").append("\n"); sb.append(" ").append(s.getName()).append("->").append(FINAL_STATE); if (countFinal > 0) { sb.append(countFinal); } sb.append("[label=\"\"]").append("\n"); countFinal++; } } } sb.append("}"); return sb.toString(); } }