/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kie.workbench.common.stunner.core.validation.impl;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.kie.workbench.common.stunner.core.api.DefinitionManager;
import org.kie.workbench.common.stunner.core.graph.Edge;
import org.kie.workbench.common.stunner.core.graph.Element;
import org.kie.workbench.common.stunner.core.graph.Graph;
import org.kie.workbench.common.stunner.core.graph.Node;
import org.kie.workbench.common.stunner.core.graph.content.definition.Definition;
import org.kie.workbench.common.stunner.core.graph.content.definition.DefinitionSet;
import org.kie.workbench.common.stunner.core.graph.content.relationship.Child;
import org.kie.workbench.common.stunner.core.graph.content.relationship.Dock;
import org.kie.workbench.common.stunner.core.graph.content.view.View;
import org.kie.workbench.common.stunner.core.graph.processing.traverse.tree.AbstractTreeTraverseCallback;
import org.kie.workbench.common.stunner.core.graph.processing.traverse.tree.TreeWalkTraverseProcessor;
import org.kie.workbench.common.stunner.core.rule.RuleManager;
import org.kie.workbench.common.stunner.core.rule.RuleSet;
import org.kie.workbench.common.stunner.core.rule.RuleViolation;
import org.kie.workbench.common.stunner.core.rule.RuleViolations;
import org.kie.workbench.common.stunner.core.rule.context.EdgeCardinalityContext;
import org.kie.workbench.common.stunner.core.rule.context.impl.RuleContextBuilder;
import org.kie.workbench.common.stunner.core.rule.violations.EmptyConnectionViolation;
import org.kie.workbench.common.stunner.core.validation.GraphValidator;
@ApplicationScoped
public class GraphValidatorImpl
implements GraphValidator<Graph, RuleViolation> {
private static Logger LOGGER = Logger.getLogger(GraphValidatorImpl.class.getName());
private final DefinitionManager definitionManager;
private final TreeWalkTraverseProcessor treeWalkTraverseProcessor;
private final RuleManager ruleManager;
protected GraphValidatorImpl() {
this(null,
null,
null);
}
@Inject
public GraphValidatorImpl(final DefinitionManager definitionManager,
final RuleManager ruleManager,
final TreeWalkTraverseProcessor treeWalkTraverseProcessor) {
this.definitionManager = definitionManager;
this.ruleManager = ruleManager;
this.treeWalkTraverseProcessor = treeWalkTraverseProcessor;
}
@Override
@SuppressWarnings("unchecked")
public void validate(final Graph graph,
final Consumer<Collection<RuleViolation>> callback) {
validate(graph,
null,
callback);
}
@Override
public void validate(final Graph graph,
final RuleSet ruleSet,
final Consumer<Collection<RuleViolation>> callback) {
this.validate(graph,
Optional.ofNullable(ruleSet),
Optional.empty(),
Optional.empty(),
Optional.empty(),
callback);
}
/**
* Performs the validation for the <code>graph</code> instance.
* @param graph The instance to validate.
* @param aRuleSet An optional rule set instance to validate against it. If not present, the default
* rule set for the the graph will be used.
* @param graphValidatorConsumer An optional consumer for the graph instance when is being validated.
* @param nodeValidatorConsumer An optional consumer each node instance when being validated.
* @param edgeValidatorConsumer An optional consumer each edge instance when being validated.
* @param resultConsumer The consumer for all the resulting validation violations produced during the
* validator for the graph, and all of its nodes and edges. It's being called once the
* validation has been completed.
*/
@SuppressWarnings("unchecked")
void validate(final Graph graph,
final Optional<RuleSet> aRuleSet,
final Optional<BiConsumer<Graph, Collection<RuleViolation>>> graphValidatorConsumer,
final Optional<BiConsumer<Node, Collection<RuleViolation>>> nodeValidatorConsumer,
final Optional<BiConsumer<Edge, Collection<RuleViolation>>> edgeValidatorConsumer,
Consumer<Collection<RuleViolation>> resultConsumer) {
final RuleSet ruleSet = aRuleSet.orElse(getRuleSet(graph));
final ViolationsSet violations = new ViolationsSet();
treeWalkTraverseProcessor
.useEdgeVisitorPolicy(TreeWalkTraverseProcessor.EdgeVisitorPolicy.VISIT_EDGE_BEFORE_TARGET_NODE)
.traverse(graph,
new AbstractTreeTraverseCallback<org.kie.workbench.common.stunner.core.graph.Graph, Node, Edge>() {
private final Stack<Node> currentParents = new Stack<Node>();
@Override
public void
startGraphTraversal(final org.kie.workbench.common.stunner.core.graph.Graph graph) {
super.startGraphTraversal(graph);
currentParents.clear();
// Evaluate the graph's cardinality rules.
final Set<RuleViolation> graphCardinalityViolations =
violations.addViolations(evaluateCardinality(ruleSet,
graph));
graphValidatorConsumer.ifPresent(g -> g.accept(graph,
graphCardinalityViolations));
}
@Override
public boolean startEdgeTraversal(final Edge edge) {
super.startEdgeTraversal(edge);
final Object content = edge.getContent();
final ViolationsSet edgeViolations =
new ViolationsSet();
if (content instanceof Child) {
this.currentParents.push(edge.getSourceNode());
} else if (content instanceof View) {
final Optional<Node<? extends View<?>, ? extends Edge>> sourceOpt =
Optional.ofNullable(edge.getSourceNode());
final Optional<Node<? extends View<?>, ? extends Edge>> targetOpt =
Optional.ofNullable(edge.getTargetNode());
// Check not empty connections.
final Optional<RuleViolation> emptyConnectionViolation =
evaluateNotEmptyConnections(graph,
edge,
sourceOpt,
targetOpt);
emptyConnectionViolation.ifPresent(edgeViolations::add);
// Evaluate connection rules.
edgeViolations.addViolations(
evaluateConnection(ruleSet,
graph,
edge,
sourceOpt,
targetOpt)
);
// Evaluate connector cardinality rules for this edge.
if (null != edge.getTargetNode()) {
edgeViolations.addViolations(
evaluateIncomingEdgeCardinality(ruleSet,
graph,
edge)
);
}
if (null != edge.getSourceNode()) {
edgeViolations.addViolations(
evaluateOutgoingEdgeCardinality(ruleSet,
graph,
edge)
);
}
} else if (content instanceof Dock) {
final Node parent = edge.getSourceNode();
final Node docked = edge.getTargetNode();
// Evaluate docking rules for the source & target nodes.
edgeViolations.addViolations(evaluateDocking(ruleSet,
graph,
parent,
docked));
}
edgeValidatorConsumer.ifPresent(c -> c.accept(edge,
edgeViolations));
violations.addAll(edgeViolations);
return true;
}
@Override
public void endEdgeTraversal(final Edge edge) {
super.endEdgeTraversal(edge);
if (edge.getContent() instanceof Child) {
this.currentParents.pop();
}
}
@Override
public boolean startNodeTraversal(final Node node) {
super.startNodeTraversal(node);
final Collection<RuleViolation> nodeViolations =
evaluateNode(node,
currentParents.isEmpty() ?
null :
currentParents.peek());
nodeValidatorConsumer.ifPresent(c -> c.accept(node,
nodeViolations));
return true;
}
@Override
public void endGraphTraversal() {
super.endGraphTraversal();
// Finished - feed the consumer instance.
resultConsumer.accept(violations);
}
private Collection<RuleViolation> evaluateNode(final Node node,
final Node parent) {
// Evaluate containment rules for this node.
return violations.addViolations(evaluateContainment(ruleSet,
graph,
null != parent ? parent : graph,
node));
}
});
}
private RuleSet getRuleSet(final Graph<? extends DefinitionSet, ?> graph) {
final String defSetId = graph.getContent().getDefinition();
final Object definitionSet = definitionManager.definitionSets().getDefinitionSetById(defSetId);
return definitionManager.adapters().forRules().getRuleSet(definitionSet);
}
@SuppressWarnings("unchecked")
private RuleViolations evaluateContainment(final RuleSet ruleSet,
final Graph graph,
final Element<? extends Definition<?>> parent,
final Node candidate) {
log(" CONTAINMENT " +
"[parent=" + parent +
",candidate=" + candidate + "]");
return ruleManager
.evaluate(ruleSet,
RuleContextBuilder.GraphContexts.containment(graph,
parent,
candidate));
}
@SuppressWarnings("unchecked")
private RuleViolations evaluateCardinality(final RuleSet ruleSet,
final Graph graph) {
log(" CARDINALITY [graph=" + graph + "]");
return ruleManager
.evaluate(ruleSet,
RuleContextBuilder.GraphContexts.cardinality(graph,
Optional.empty(),
Optional.empty()));
}
@SuppressWarnings("unchecked")
private RuleViolations evaluateDocking(final RuleSet ruleSet,
final Graph graph,
final Element<? extends Definition<?>> parent,
final Node candidate) {
log(" DOCKING " +
"[parent=" + (parent.getUUID()) +
",candidate=" + candidate.getUUID() + "]");
return ruleManager
.evaluate(ruleSet,
RuleContextBuilder.GraphContexts.docking(graph,
parent,
candidate));
}
/**
* Actually the different commands allows removing nodes that have
* incoming or outgoing view connectors, as connectors can be re-attached to
* a different node, but the graph structure is not considered valid, so the
* diagram cannot be updated until fixing that connections.
* So there exist no rule for empty connections, actually it's just a common validation.
*/
@SuppressWarnings("unchecked")
private Optional<RuleViolation> evaluateNotEmptyConnections(final Graph<?, ? extends Node> graph,
final Edge<? extends View<?>, ? extends Node> connector,
final Optional<Node<? extends View<?>, ? extends Edge>> sourceNode,
final Optional<Node<? extends View<?>, ? extends Edge>> targetNode) {
log(" NOT_EMPTY_CONNECTIONS " +
"[edge=" + connector +
",source=" + sourceNode.orElse(null) +
",target=" + targetNode.orElse(null) + "]");
if (!sourceNode.isPresent() || !targetNode.isPresent()) {
return Optional.of(EmptyConnectionViolation.Builder.build(connector,
sourceNode,
targetNode));
}
return Optional.empty();
}
@SuppressWarnings("unchecked")
private RuleViolations evaluateConnection(final RuleSet ruleSet,
final Graph<?, ? extends Node> graph,
final Edge<? extends View<?>, ? extends Node> connector,
final Optional<Node<? extends View<?>, ? extends Edge>> sourceNode,
final Optional<Node<? extends View<?>, ? extends Edge>> targetNode) {
log(" CONNECTION " +
"[edge=" + connector +
",source=" + sourceNode.orElse(null) +
",target=" + targetNode.orElse(null) + "]");
return ruleManager
.evaluate(ruleSet,
RuleContextBuilder.GraphContexts.connection(graph,
connector,
sourceNode,
targetNode));
}
@SuppressWarnings("unchecked")
private RuleViolations evaluateIncomingEdgeCardinality(final RuleSet ruleSet,
final org.kie.workbench.common.stunner.core.graph.Graph graph,
final Edge<? extends View, Node> edge) {
log(" IN-EDGE CARDINALITY [edge=" + edge + "]");
return ruleManager
.evaluate(ruleSet,
RuleContextBuilder.GraphContexts.edgeCardinality(graph,
edge.getTargetNode(),
(Edge<? extends View<?>, Node>) edge,
EdgeCardinalityContext.Direction.INCOMING,
Optional.empty()));
}
@SuppressWarnings("unchecked")
private RuleViolations evaluateOutgoingEdgeCardinality(final RuleSet ruleSet,
final org.kie.workbench.common.stunner.core.graph.Graph graph,
final Edge<? extends View, Node> edge) {
log(" OUT-EDGE CARDINALITY [edge=" + edge + "]");
return ruleManager
.evaluate(ruleSet,
RuleContextBuilder.GraphContexts.edgeCardinality(graph,
edge.getSourceNode(),
(Edge<? extends View<?>, Node>) edge,
EdgeCardinalityContext.Direction.OUTGOING,
Optional.empty()));
}
private class ViolationsSet extends LinkedHashSet<RuleViolation> {
public Set<RuleViolation> addViolations(final RuleViolations items) {
final Set<RuleViolation> result = new LinkedHashSet<>();
items.violations().forEach(v -> {
result.add(v);
ViolationsSet.this.add(v);
});
return result;
}
}
private void log(final String message) {
LOGGER.info(message);
}
}