/*
* 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.lookup.util;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
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.api.FactoryManager;
import org.kie.workbench.common.stunner.core.definition.morph.MorphDefinition;
import org.kie.workbench.common.stunner.core.graph.Edge;
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.util.GraphUtils;
import org.kie.workbench.common.stunner.core.lookup.LookupManager;
import org.kie.workbench.common.stunner.core.lookup.definition.DefinitionLookupManager;
import org.kie.workbench.common.stunner.core.lookup.definition.DefinitionLookupRequest;
import org.kie.workbench.common.stunner.core.lookup.definition.DefinitionLookupRequestImpl;
import org.kie.workbench.common.stunner.core.lookup.definition.DefinitionRepresentation;
import org.kie.workbench.common.stunner.core.lookup.rule.RuleLookupManager;
import org.kie.workbench.common.stunner.core.lookup.rule.RuleLookupRequest;
import org.kie.workbench.common.stunner.core.lookup.rule.RuleLookupRequestImpl;
import org.kie.workbench.common.stunner.core.rule.Rule;
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.CardinalityContext;
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.impl.CanConnect;
import org.kie.workbench.common.stunner.core.util.DefinitionUtils;
import static org.uberfire.commons.validation.PortablePreconditions.checkNotNull;
/**
* An utils class that provides common used look-ups and other logic for querying the domain model and the rules model,
* that is used along the application.
* <p>
* // TODO: Some kind of cache to avoid frequently used lookups? Consider performance and memory, this class
* is shared on both server and client sides.
*/
@ApplicationScoped
public class CommonLookups {
private static Logger LOGGER = Logger.getLogger(CommonLookups.class.getName());
private final DefinitionUtils definitionUtils;
private final DefinitionLookupManager definitionLookupManager;
private final RuleManager ruleManager;
private final RuleLookupManager ruleLookupManager;
private final FactoryManager factoryManager;
protected CommonLookups() {
this(null,
null,
null,
null,
null);
}
@Inject
public CommonLookups(final DefinitionUtils definitionUtils,
final RuleManager ruleManager,
final DefinitionLookupManager definitionLookupManager,
final RuleLookupManager ruleLookupManager,
final FactoryManager factoryManager) {
this.definitionUtils = definitionUtils;
this.ruleManager = ruleManager;
this.definitionLookupManager = definitionLookupManager;
this.ruleLookupManager = ruleLookupManager;
this.factoryManager = factoryManager;
}
/**
* Returns the allowed edge definition identifiers that can be added as outgoing edges for the given source node.
*/
public <T> Set<String> getAllowedConnectors(final String defSetId,
final Node<Definition<T>, Edge> sourceNode,
final int page,
final int pageSize) {
final Set<String> result = new LinkedHashSet<>();
if (null != defSetId && null != sourceNode) {
final T definition = sourceNode.getContent().getDefinition();
final Set<String> connectionAllowedEdges = getConnectionRulesAllowedEdges(defSetId,
definition,
page,
pageSize);
if (null != connectionAllowedEdges && !connectionAllowedEdges.isEmpty()) {
final RuleSet ruleSet = getRuleSet(defSetId);
connectionAllowedEdges.stream().forEach(allowedEdgeId -> {
final int edgeCount = countOutgoingEdges(sourceNode,
allowedEdgeId);
final boolean oeCardinalityAllowed = getDefinitionLabels(definition).stream()
.filter(role -> pass(ruleManager.evaluate(ruleSet,
RuleContextBuilder.DomainContexts.edgeCardinality(sourceNode.getLabels(),
allowedEdgeId,
edgeCount,
EdgeCardinalityContext.Direction.OUTGOING,
Optional.of(CardinalityContext.Operation.ADD)))))
.findAny()
.isPresent();
log(Level.FINEST,
"Outgoing edge cardinality rules evaluation - Result = [" + oeCardinalityAllowed + "]");
if (oeCardinalityAllowed) {
result.add(allowedEdgeId);
}
});
}
}
return result;
}
private RuleSet getRuleSet(final String defSetId) {
checkNotNull("defSetId",
defSetId);
final Object definitionSet = getDefinitionManager().definitionSets().getDefinitionSetById(defSetId);
return getDefinitionManager()
.adapters()
.registry()
.getDefinitionSetRuleAdapter(definitionSet.getClass())
.getRuleSet(definitionSet);
}
/**
* Returns the allowed definition identifiers that can be used as target node for the given source node and
* the given edge (connector) identifier.
* This method only returns the definition identifiers that are considered the default types for its morph type,
* it does NOT return all the identifiers for all the allowed target definitions.
* <p>
* TODO: Handle several result pages.
*/
public <T> Set<String> getAllowedMorphDefaultDefinitions(final String defSetId,
final Graph<?, ? extends Node> graph,
final Node<? extends Definition<T>, ? extends Edge> sourceNode,
final String edgeId,
final int page,
final int pageSize) {
final Set<Object> allowedDefinitions = getAllowedTargetDefinitions(defSetId,
graph,
sourceNode,
edgeId,
page,
pageSize);
log(Level.FINEST,
"Target definitions allowed " +
"for [" + sourceNode + "] and using the " +
"connector [" + edgeId + "] " +
"ARE [" + allowedDefinitions + "]");
if (null != allowedDefinitions) {
final Set<String> result = new LinkedHashSet<>();
allowedDefinitions.stream().forEach(definition -> {
final String defId = getDefinitionManager().adapters().forDefinition().getId(definition);
final MorphDefinition morphDefinition = definitionUtils.getMorphDefinition(definition);
final boolean hasMorphBase = null != morphDefinition;
final String id = hasMorphBase ? morphDefinition.getDefault() : defId;
result.add(id);
});
log(Level.FINEST,
"Target definitions group by morph base type allowed " +
"for [" + sourceNode + "] and using the " +
"connector [" + edgeId + "] " +
"ARE [" + result + "]");
return result;
}
return null;
}
/**
* Returns the allowed definition identifiers that can be used as target node for the given source node and
* the given edge (connector) identifier.
* <p>
* TODO: Handle several result pages.
*/
@SuppressWarnings("unchecked")
public <T> Set<Object> getAllowedTargetDefinitions(final String defSetId,
final Graph<?, ? extends Node> graph,
final Node<? extends Definition<T>, ? extends Edge> sourceNode,
final String edgeId,
final int page,
final int pageSize) {
if (null != defSetId && null != graph && null != sourceNode && null != edgeId) {
final T definition = sourceNode.getContent().getDefinition();
final RuleSet ruleSet = getRuleSet(defSetId);
log(Level.FINEST,
"*** Checking the target definitions allowed " +
"for [" + definition + "] and using the " +
"connector [" + edgeId + "] ***");
// Check outgoing connectors cardinality for the source node ( plus the new one to be added ).
final int outConnectorsCount = countOutgoingEdges(sourceNode,
edgeId);
log(Level.FINEST,
"The source node has " + outConnectorsCount + "] outgoing connections.");
final RuleViolations oev =
ruleManager.evaluate(ruleSet,
RuleContextBuilder.DomainContexts.edgeCardinality(sourceNode.getLabels(),
edgeId,
outConnectorsCount,
EdgeCardinalityContext.Direction.OUTGOING,
Optional.of(CardinalityContext.Operation.ADD)));
final boolean oeCardinalityAllowed = pass(oev);
log(Level.FINEST,
"Outgoing edge cardinality rules evaluation " +
"result = [" + oeCardinalityAllowed + "]");
if (oeCardinalityAllowed) {
// Obtain allowed target roles that pass connection rules.
final Set<String> allowedConnectionRoles = getConnectionRulesAllowedTargets(defSetId,
definition,
edgeId,
page,
pageSize);
log(Level.FINEST,
"Allowed target roles that pass connection rules " +
"ARE [" + allowedConnectionRoles + "]");
if (null != allowedConnectionRoles) {
// Obtain a first set of candidate Defintiion identifiers.
final Set<String> allowedDefinitions = getDefinitions(defSetId,
allowedConnectionRoles);
log(Level.FINEST,
"Allowed target definitions that pass connection rules " +
"ARE [" + allowedConnectionRoles + "]");
if (null != allowedDefinitions) {
final Map<String, Integer> graphLabelCount = GraphUtils.getLabelsCount(graph,
allowedConnectionRoles);
final int inConnectorsCount = countIncomingEdges(sourceNode,
edgeId);
final Set<Object> result = new LinkedHashSet<>();
allowedDefinitions
.stream()
.forEach(defId -> {
final Object targetDefinition = createDefinition(defId);
if (null != targetDefinition) {
final Set<String> targetDefinitionRoles =
getDefinitionManager()
.adapters()
.forDefinition()
.getLabels(targetDefinition);
// Check cardinality for each of the roles for this potential target node.
final boolean hasCardinalityViolations = targetDefinitionRoles
.stream()
.filter(role -> {
final Integer roleCount = Optional.ofNullable(graphLabelCount.get(role)).orElse(0);
final RuleViolations violations =
ruleManager.evaluate(ruleSet,
RuleContextBuilder.DomainContexts.cardinality(Collections.singleton(role),
roleCount,
Optional.of(CardinalityContext.Operation.ADD)));
return !pass(violations);
})
.findFirst()
.isPresent();
log(Level.FINEST,
"Cardinality rules evaluation " +
"result = [" + hasCardinalityViolations + "]");
if (!hasCardinalityViolations) {
// Check incoming connector cardinality for each the target node.
final RuleViolations iev =
ruleManager.evaluate(ruleSet,
RuleContextBuilder.DomainContexts.edgeCardinality(Collections.singleton(defId),
edgeId,
inConnectorsCount,
EdgeCardinalityContext.Direction.INCOMING,
Optional.of(CardinalityContext.Operation.ADD)));
final boolean ieCardinalityAllowed = pass(iev);
log(Level.FINEST,
"Incoming edge cardinality rules evaluation " +
"result = [" + ieCardinalityAllowed + "]");
if (ieCardinalityAllowed) {
// This potential node can be used as target one, as it passes all rule checks.
result.add(targetDefinition);
}
}
}
});
return result;
}
}
}
}
return null;
}
/**
* Returns all the Definition Set's definition identifiers that contains the given labels.
* <p>
* TODO: Handle several result pages.
*/
private Set<String> getDefinitions(final String defSetId,
final Set<String> labels) {
if (null != labels && !labels.isEmpty()) {
final DefinitionLookupRequest request =
new DefinitionLookupRequestImpl.Builder()
.definitionSetId(defSetId)
.labels(labels)
.page(0)
.pageSize(100)
.build();
final LookupManager.LookupResponse<DefinitionRepresentation> response = definitionLookupManager.lookup(request);
final List<DefinitionRepresentation> definitionRepresentations = response.getResults();
if (null != definitionRepresentations && !definitionRepresentations.isEmpty()) {
final Set<String> result = new LinkedHashSet<>();
for (final DefinitionRepresentation definitionRepresentation : definitionRepresentations) {
final String id = definitionRepresentation.getDefinitionId();
result.add(id);
}
return result;
}
}
return new HashSet<>(0);
}
/**
* Returns the allowed edge identifiers that satisfy connection rules for the given
* source definition.
* @oaram sourceDefinition The domain model object ( not a graph element ).
*/
private <T> Set<String> getConnectionRulesAllowedEdges(final String defSetId,
final T sourceDefinition,
final int page,
final int pageSize) {
final List<Rule> rules = lookupConnectionRules(defSetId,
sourceDefinition,
null,
page,
pageSize);
if (null != rules && !rules.isEmpty()) {
final Set<String> result = new LinkedHashSet<>();
for (final Rule rule : rules) {
final CanConnect cr = (CanConnect) rule;
final String edgeId = cr.getRole();
result.add(edgeId);
}
return result;
}
return null;
}
/**
* Returns the allowed ROLES that satisfy connection rules for a given source
* definition ( domain model object, not a node ).and the given edge (connector) identifier.
* <p>
* TODO: Handle several result pages.
*/
private <T> Set<String> getConnectionRulesAllowedTargets(final String defSetId,
final T sourceDefinition,
final String edgeId,
final int page,
final int pageSize) {
final List<Rule> rules = lookupConnectionRules(defSetId,
sourceDefinition,
edgeId,
page,
pageSize);
if (null != rules && !rules.isEmpty()) {
final Set<String> result = new LinkedHashSet<>();
for (final Rule rule : rules) {
final CanConnect cr = (CanConnect) rule;
final List<CanConnect.PermittedConnection> connections = cr.getPermittedConnections();
if (null != connections && !connections.isEmpty()) {
for (final CanConnect.PermittedConnection connection : connections) {
result.add(connection.getEndRole());
}
}
}
return result;
}
return null;
}
private <T> List<Rule> lookupConnectionRules(final String defSetId,
final T sourceDefinition,
final String edgeId,
final int page,
final int pageSize) {
if (null != defSetId) {
final Set<String> defLabels = getDefinitionLabels(sourceDefinition);
final RuleLookupRequestImpl.Builder builder = new RuleLookupRequestImpl.Builder();
builder.definitionSetId(defSetId)
.type(RuleLookupRequestImpl.Builder.RuleType.CONNECTION)
.from(defLabels)
.page(page)
.pageSize(pageSize);
if (null != edgeId) {
builder.id(edgeId);
}
final RuleLookupRequest request = builder.build();
final LookupManager.LookupResponse<Rule> response = ruleLookupManager.lookup(request);
return response.getResults();
}
return null;
}
private <T> int countIncomingEdges(final Node<? extends Definition<T>, ? extends Edge> sourceNode,
final String edgeId) {
final List<? extends Edge> edges = sourceNode.getInEdges();
return GraphUtils.countEdges(getDefinitionManager(),
edgeId,
edges);
}
private <T> int countOutgoingEdges(final Node<? extends Definition<T>, ? extends Edge> sourceNode,
final String edgeId) {
final List<? extends Edge> edges = sourceNode.getOutEdges();
return GraphUtils.countEdges(getDefinitionManager(),
edgeId,
edges);
}
private <T> Set<String> getDefinitionLabels(final T definition) {
return getDefinitionManager().adapters().forDefinition().getLabels(definition);
}
private boolean pass(final RuleViolations violations) {
return null == violations || !violations.violations(RuleViolation.Type.ERROR).iterator().hasNext();
}
private Object createDefinition(final String defId) {
// TODO: Avoid new instances here.
return factoryManager.newDefinition(defId);
}
private DefinitionManager getDefinitionManager() {
return definitionUtils.getDefinitionManager();
}
private void log(final Level level,
final String message) {
LOGGER.log(level,
message);
}
}