package org.eclipse.mdm.nodeprovider.control; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.List; | |
import java.util.stream.Collectors; | |
import org.eclipse.mdm.api.base.ServiceNotProvidedException; | |
import org.eclipse.mdm.api.base.adapter.Attribute; | |
import org.eclipse.mdm.api.base.adapter.EntityType; | |
import org.eclipse.mdm.api.base.adapter.ModelManager; | |
import org.eclipse.mdm.api.base.model.Channel; | |
import org.eclipse.mdm.api.base.model.ChannelGroup; | |
import org.eclipse.mdm.api.base.model.Entity; | |
import org.eclipse.mdm.api.base.model.Environment; | |
import org.eclipse.mdm.api.base.model.Measurement; | |
import org.eclipse.mdm.api.base.model.Quantity; | |
import org.eclipse.mdm.api.base.model.Test; | |
import org.eclipse.mdm.api.base.model.TestStep; | |
import org.eclipse.mdm.api.base.model.Value; | |
import org.eclipse.mdm.api.base.model.ValueType; | |
import org.eclipse.mdm.api.base.query.BooleanOperator; | |
import org.eclipse.mdm.api.base.query.ComparisonOperator; | |
import org.eclipse.mdm.api.base.query.Filter; | |
import org.eclipse.mdm.api.base.query.FilterItem; | |
import org.eclipse.mdm.api.base.query.Query; | |
import org.eclipse.mdm.api.base.query.QueryService; | |
import org.eclipse.mdm.api.base.query.Result; | |
import org.eclipse.mdm.api.base.search.ContextState; | |
import org.eclipse.mdm.api.base.search.SearchService; | |
import org.eclipse.mdm.api.dflt.ApplicationContext; | |
import org.eclipse.mdm.api.dflt.model.Pool; | |
import org.eclipse.mdm.api.dflt.model.Project; | |
import org.eclipse.mdm.businessobjects.control.FilterParser; | |
import org.eclipse.mdm.businessobjects.utils.Serializer; | |
import org.eclipse.mdm.businessobjects.utils.ServiceUtils; | |
import org.eclipse.mdm.connector.boundary.ConnectorService; | |
import org.eclipse.mdm.nodeprovider.entity.NodeLevel; | |
import org.eclipse.mdm.nodeprovider.entity.NodeProvider; | |
import org.eclipse.mdm.nodeprovider.entity.NodeProviderRoot; | |
import org.eclipse.mdm.nodeprovider.entity.Order; | |
import org.eclipse.mdm.nodeprovider.utils.SerializationUtil; | |
import org.eclipse.mdm.protobuf.Mdm.Node; | |
import com.google.common.base.Joiner; | |
/** | |
* Configurable implementation of a {@link NodeProvider}. A | |
* {@link NodeProviderRoot} must be provided, which defines the structure of the | |
* tree defined by the returned {@link Node}s. | |
* | |
*/ | |
public class GenericNodeProvider implements NodeProvider { | |
private ConnectorService connectorService; | |
private NodeProviderRoot root; | |
/** | |
* Construct a new {@link GenericNodeProvider} using the given | |
* {@link ConnectorService} and {@link NodeProviderRoot}. | |
* | |
* @param connectorService | |
* @param root | |
*/ | |
public GenericNodeProvider(ConnectorService connectorService, NodeProviderRoot root) { | |
this.connectorService = connectorService; | |
this.root = root; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public String getName() { | |
return root.getName(); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public List<Node> getRoots() { | |
return getChildren(null); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public List<Node> getChildren(Node parent) { | |
List<Node> nodes = new ArrayList<>(); | |
for (ApplicationContext context : this.connectorService.getContexts()) { | |
root.getChildNodeLevel(context, parent).map(nl -> loadChildNodes(context, parent, nl)) | |
.ifPresent(nodes::addAll); | |
} | |
// Sort the complete list again, as only Nodes from each context are sorted. | |
// TODO 30.11.2020: sort by defined OrderAttributes | |
nodes.sort(compareByLabel); | |
return nodes; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public List<Node> getTreePath(Node node) { | |
ApplicationContext context = this.connectorService.getContextByName(node.getSource()); | |
NodeLevel nodeLevel = root.getNodeLevel(context, node.getType(), node.getIdAttribute()); | |
List<Node> nodes = new ArrayList<>(); | |
// reload node to get correct label | |
Query query = createQueryForNodeLevel(context, nodeLevel); | |
for (Result r : query.fetch(createFilterFromIdOrLabel(node, nodeLevel))) { | |
nodes.add(convertNode(r, node.getSource(), nodeLevel, Filter.and())); | |
} | |
if (nodes.size() != 1) { | |
throw new NodeProviderException("Expected exactly one node, but got " + nodes.size() + "!"); | |
} | |
Node parent = nodes.get(0); | |
while ((parent = loadParentNode(context, parent, nodeLevel)) != null) { | |
nodes.add(parent); | |
nodeLevel = root.getNodeLevel(context, parent.getType(), parent.getIdAttribute()); | |
} | |
return reverseListAndFilters(nodes); | |
} | |
/** | |
* Loads the child node of the given parent Node and NodeLevel. | |
* | |
* @param context ApplicationContext | |
* @param parent parent node | |
* @param childNodeLevel nodeLevel of the child nodes | |
* @return the children of the given node | |
*/ | |
private List<Node> loadChildNodes(ApplicationContext context, Node parent, NodeLevel childNodeLevel) { | |
String sourceName = context.getSourceName(); | |
Filter filter = getFilter(context, parent); | |
if (isEnvironment(childNodeLevel.getEntityType())) { | |
return createQueryForNodeLevel(context, childNodeLevel).fetch().stream() | |
.map(r -> convertNode(r, sourceName, childNodeLevel, filter)).collect(Collectors.toList()); | |
} | |
if (!sourceName.equalsIgnoreCase(parent.getSource()) || !sourceNameMatches(filter, sourceName)) { | |
return Collections.emptyList(); | |
} | |
if (childNodeLevel.isVirtual()) { | |
// We use the TestStep entity as query root to request the values for the label | |
List<Value> values = getSearchService(context).getFilterValues(TestStep.class, | |
childNodeLevel.getLabelAttribute()); | |
// Filter values are always in measured | |
List<Node> nodes = new ArrayList<>(); | |
for (Value v : values) { | |
nodes.add(convertNode(v, sourceName, childNodeLevel, filter)); | |
} | |
return nodes; | |
} else { | |
return fetchNodes(context, childNodeLevel, filter); | |
} | |
} | |
/** | |
* Loads the parent node of the given Node and NodeLevel. | |
* | |
* @param context | |
* @param node | |
* @param nodeLevel | |
* @return the parent node of the given node, or null if the given node is a | |
* root node. | |
*/ | |
protected Node loadParentNode(ApplicationContext context, Node node, NodeLevel nodeLevel) { | |
NodeLevel parentLevel = root.getParentNodeLevel(context, nodeLevel); | |
if (parentLevel == null) { | |
return null; | |
} | |
Filter childFilter = getFilter(context, node); | |
if (childFilter.isEmtpty()) { | |
throw new NodeProviderException("Filter cannot be empty! Received node '" + node + "' with empty filter."); | |
} | |
if (isEnvironment(parentLevel.getEntityType())) { | |
Query query = createQueryForNodeLevel(context, parentLevel); | |
for (Result r : query.fetch()) { | |
return convertNode(r, context.getSourceName(), parentLevel, childFilter); | |
} | |
return null; | |
} | |
List<Node> nodes = new ArrayList<>(); | |
if (parentLevel.isVirtual()) { | |
// We use the TestStep entity as query root to request the values for the label | |
List<Result> results = getSearchService(context).fetchResults(TestStep.class, | |
Arrays.asList(parentLevel.getLabelAttribute()), childFilter, ""); | |
for (Result r : results) { | |
Value value = r.getValue(parentLevel.getLabelAttribute()); | |
nodes.add(convertNode(value, node.getSource(), parentLevel, childFilter)); | |
break; | |
} | |
} else { | |
nodes.addAll(fetchNodes(context, parentLevel, childFilter)); | |
} | |
if (nodes.size() != 1) { | |
throw new NodeProviderException("Expected exactly one node but got " + nodes.size() + "!"); | |
} | |
return nodes.get(0); | |
} | |
/** | |
* Creates an ID filter for non virtual nodes or a filter based on the label for | |
* virtual nodes. | |
* | |
* @param node Node to build the Filter for | |
* @param nodeLevel NodeLevel of the Node | |
* @return created Filter | |
*/ | |
private Filter createFilterFromIdOrLabel(Node node, NodeLevel nodeLevel) { | |
if (nodeLevel.isVirtual()) { | |
return Filter.and().add(ComparisonOperator.EQUAL.create(nodeLevel.getContextState(), | |
nodeLevel.getLabelAttribute(), node.getLabel())); | |
} else { | |
return Filter.and().id(nodeLevel.getEntityType(), node.getId()); | |
} | |
} | |
/** | |
* Reverse the Nodes and build up the filters of the individual Node from root | |
* to leaf. | |
* | |
* @param treePath tree path build from leaf to root. | |
* @return list of Nodes build from root to leaf, e.g. the root will be the | |
* first Node in the returned list and the leaf will be the last Node. | |
*/ | |
private List<Node> reverseListAndFilters(List<Node> treePath) { | |
List<Node> finalList = new ArrayList<>(); | |
List<String> filters = new ArrayList<>(); | |
for (int i = treePath.size() - 1; i >= 0; i--) { | |
String current = treePath.get(i).getFilter(); | |
if (i - 1 >= 0) { | |
String child = treePath.get(i - 1).getFilter(); | |
if (!current.equals(child)) { | |
filters.add(current.replace(child + " and ", "")); | |
} | |
} else { | |
filters.add(current); | |
} | |
Node node = treePath.get(i); | |
finalList.add(SerializationUtil.createNode(node.getSource(), node.getType(), node.getId(), | |
node.getIdAttribute(), Joiner.on(" and ").join(filters), node.getLabel())); | |
} | |
return finalList; | |
} | |
/** | |
* @param context ApplicationContext | |
* @param nodeLevel nodeLevel to search for nodes | |
* @param filter filter used for the search | |
* @return list with the loaded nodes | |
*/ | |
private List<Node> fetchNodes(ApplicationContext context, NodeLevel nodeLevel, Filter filter) { | |
Class<? extends Entity> entityClass = getEntityClass(nodeLevel.getIdAttribute().getEntityType()); | |
List<Result> results = getSearchService(context).fetchResults(entityClass, | |
Arrays.asList(nodeLevel.getIdAttribute(), nodeLevel.getLabelAttribute()), filter, ""); | |
return results.stream().map(r -> convertNode(r, context.getSourceName(), nodeLevel, filter)) | |
.collect(Collectors.toList()); | |
} | |
/** | |
* @param entityType | |
* @return entity class of the given entityType | |
* @throws NodeProviderException if entity class is not found | |
*/ | |
private Class<? extends Entity> getEntityClass(EntityType entityType) { | |
String entityName = ServiceUtils.workaroundForTypeMapping(entityType); | |
switch (entityName) { | |
case "Environment": | |
return Environment.class; | |
case "Project": | |
return Project.class; | |
case "Pool": | |
return Pool.class; | |
case "Test": | |
return Test.class; | |
case "TestStep": | |
return TestStep.class; | |
case "Measurement": | |
return Measurement.class; | |
case "ChannelGroup": | |
return ChannelGroup.class; | |
case "Channel": | |
return Channel.class; | |
case "Quantity": | |
return Quantity.class; | |
default: | |
throw new RuntimeException("Could not find entity class for entity with name '" + entityName + "'."); | |
} | |
} | |
/** | |
* Checks if the given filter matches the given sourceName | |
* | |
* @param filter | |
* @param sourceName | |
* @return true, if the sourceName could match the filter. | |
*/ | |
private boolean sourceNameMatches(Filter filter, String sourceName) { | |
if (filter.isEmtpty()) { | |
return true; | |
} | |
for (FilterItem filterItem : filter) { | |
if (filterItem.isCondition()) { | |
Attribute a = filterItem.getCondition().getAttribute(); | |
if (isEnvironment(a.getEntityType()) && "Id".equals(a.getName())) { | |
return sourceName.equals(filterItem.getCondition().getValue().extract(ValueType.STRING)); | |
} | |
} else if (filterItem.isBooleanOperator() && filterItem.getBooleanOperator() == BooleanOperator.AND) { | |
continue; | |
} else { | |
throw new NodeProviderException("Filter not supported yet: " + filter); | |
} | |
} | |
return true; | |
} | |
/** | |
* Returns the filter of the given node. | |
* | |
* @param context application context | |
* @param node | |
* @return the filter of the node or an empty Filter | |
*/ | |
private Filter getFilter(ApplicationContext context, Node node) { | |
Filter filter = Filter.and(); | |
if (node != null) { | |
String filterStr = node.getFilter(); | |
if (!filterStr.isEmpty()) { | |
ModelManager mm = context.getModelManager() | |
.orElseThrow(() -> new ServiceNotProvidedException(ModelManager.class)); | |
filter = FilterParser.parseFilterString(mm.listEntityTypes(), filterStr); | |
} | |
} | |
return filter; | |
} | |
/** | |
* Converts a Result to a Node | |
* | |
* @param r Result to convert | |
* @param sourceName name of the source | |
* @param nodeLevel NodeLevel | |
* @param parentFilter Filter of the parent Node | |
* @return Result converted to a Node | |
*/ | |
private Node convertNode(Result r, String sourceName, NodeLevel nodeLevel, Filter parentFilter) { | |
String label = r.getValue(nodeLevel.getLabelAttribute()).extract(nodeLevel.getContextState()); | |
String id = r.getValue(nodeLevel.getIdAttribute()).extract(nodeLevel.getContextState()); | |
Filter newFilter = null; | |
if (parentFilter != null) { | |
if (isEnvironment(nodeLevel.getEntityType())) { | |
newFilter = parentFilter.copy(); | |
} else { | |
newFilter = parentFilter.copy().add( | |
ComparisonOperator.EQUAL.create(nodeLevel.getContextState(), nodeLevel.getIdAttribute(), id)); | |
} | |
} | |
return SerializationUtil.createNode(sourceName, | |
ServiceUtils.workaroundForTypeMapping(nodeLevel.getEntityType()), id, | |
nodeLevel.getIdAttribute().getName(), newFilter, label); | |
} | |
/** | |
* Converts a Value to a Node. | |
* | |
* @param v value to convert | |
* @param sourceName name of the source | |
* @param nodeLevel NodeLevel | |
* @param parentFilter Filter of the parent Node | |
* @return Value converted to a Node | |
*/ | |
private Node convertNode(Value v, String sourceName, NodeLevel nodeLevel, Filter parentFilter) { | |
ContextState contextState = nodeLevel.getContextState() == null ? ContextState.MEASURED | |
: nodeLevel.getContextState(); | |
String label = Serializer.serializeValue(v).toString(); | |
Filter newFilter = null; | |
if (isEnvironment(nodeLevel.getEntityType())) { | |
// only 1 environment per context -> no parent filter needed | |
newFilter = Filter.and(); | |
} else if (parentFilter != null) { | |
newFilter = parentFilter.copy() | |
.add(ComparisonOperator.EQUAL.create(contextState, nodeLevel.getLabelAttribute(), v.extract())); | |
} | |
return SerializationUtil.createNode(sourceName, | |
ServiceUtils.workaroundForTypeMapping(nodeLevel.getEntityType()), label, | |
nodeLevel.getIdAttribute().getName(), newFilter, label); | |
} | |
/** | |
* Creates a Query to load nodes for the given NodeLevel | |
* | |
* @param context ApplicationContext in which the query is created | |
* @param nodeLevel the NodeLevel of the requested nodes. | |
* @return Query for the given NodeLevel | |
*/ | |
private Query createQueryForNodeLevel(ApplicationContext context, NodeLevel nodeLevel) { | |
Query query = context.getQueryService().orElseThrow(() -> new ServiceNotProvidedException(QueryService.class)) | |
.createQuery().select(nodeLevel.getIdAttribute(), nodeLevel.getLabelAttribute()) | |
.group(nodeLevel.getIdAttribute(), nodeLevel.getLabelAttribute()); | |
nodeLevel.getOrderAttributes().stream() | |
.forEach(oa -> query.order(oa.getAttribute(), oa.getOrder() == Order.ASCENDING)); | |
return query; | |
} | |
/** | |
* Returns the SearchService | |
* | |
* @param context ApplicationContext | |
* @return the SearchService | |
* @throws ServiceNotProvidedException if no SearchService is available from the | |
* context. | |
*/ | |
private SearchService getSearchService(ApplicationContext context) { | |
return context.getSearchService().orElseThrow(() -> new ServiceNotProvidedException(SearchService.class)); | |
} | |
/** | |
* Checks if entity type is an Environment | |
* | |
* @param entityType entityType to check | |
* @return true, if entity type is an Environment | |
*/ | |
private boolean isEnvironment(EntityType entityType) { | |
return Environment.class.getSimpleName().equals(entityType.getName()); | |
} | |
/** | |
* Compares {@link Node}s be {@link Node#getLabel()}. | |
*/ | |
public static final Comparator<Node> compareByLabel = new Comparator<Node>() { | |
@Override | |
public int compare(Node o1, Node o2) { | |
return o1.getLabel().compareTo(o2.getLabel()); | |
} | |
}; | |
} |