Merge branch 'mkoller/nodeprovider' into mkoller/webclient

Conflicts:
	api/atfxadapter/openatfx/build.gradle

Change-Id: Ieab74c9222711b539e0f6d962208824631f54116
diff --git a/api/atfxadapter/openatfx/build.gradle b/api/atfxadapter/openatfx/build.gradle
index 912e114..c0a9e5c 100644
--- a/api/atfxadapter/openatfx/build.gradle
+++ b/api/atfxadapter/openatfx/build.gradle
@@ -31,8 +31,9 @@
 	compile project(":api:odsadapter")
 	testCompile 'junit:junit:4.12'
 	testCompile 'org.apache.commons:commons-math:2.2'
-	testCompile 'org.assertj:assertj-core:3.6.2'
 	testCompile 'org.mockito:mockito-core:2.13.0'
+	testCompile 'org.assertj:assertj-core:3.6.2'
+
 }
 
 configurations.all {
@@ -64,9 +65,15 @@
 	outputs.dir file("${buildDir}/openatfx-${atfxVersion}")
 }
 
+
 task copySource(dependsOn: unzipOpenATFX, type: Copy) {
 	from file("${buildDir}/openatfx-${atfxVersion}/src")
 	into 'src'
+	/*
+	 * openATFX is implemented against the ODS API compiled by JacORB. The JacORB IDL compiler generates SelItem#operator() while SunORB IDL compiler generates SelItem#_operator().
+	 * Since openMDM uses SunORB to generate the ODS API from IDL, openATFX source code is missing SelItem#operator(). Therefore we change it with a simple replace and add the underscore.
+	 */
+	filter { line -> line.replaceAll('SelOperator.AND.equals\\(condition.operator\\(\\)', 'SelOperator.AND.equals\\(condition._operator\\(\\)').replaceAll('return new TS_ValueSeq\\(new TS_UnionSeq\\(\\), new short\\[0\\]\\)', 'return ODSHelper.tsValue2tsValueSeq\\(new TS_Value\\[0\\], dt\\)') }
 }
 
 compileJava.dependsOn copySource
diff --git a/api/base/src/main/java/org/eclipse/mdm/api/base/adapter/Attribute.java b/api/base/src/main/java/org/eclipse/mdm/api/base/adapter/Attribute.java
index 0771091..6d50c8c 100644
--- a/api/base/src/main/java/org/eclipse/mdm/api/base/adapter/Attribute.java
+++ b/api/base/src/main/java/org/eclipse/mdm/api/base/adapter/Attribute.java
@@ -108,7 +108,7 @@
 	 * @return Created {@code Value} is returned.
 	 */
 	default Value createValue(String unit, Object input) {
-		return createValue(unit, true, input);
+		return createValue(unit, input != null, input);
 	}
 
 	/**
diff --git a/api/base/src/main/java/org/eclipse/mdm/api/base/query/ComparisonOperator.java b/api/base/src/main/java/org/eclipse/mdm/api/base/query/ComparisonOperator.java
index 8c80acd..d01417f 100644
--- a/api/base/src/main/java/org/eclipse/mdm/api/base/query/ComparisonOperator.java
+++ b/api/base/src/main/java/org/eclipse/mdm/api/base/query/ComparisonOperator.java
@@ -253,7 +253,7 @@
 		case IN_SET:
 			return "in";
 		case IS_NOT_NULL:
-			return "not_in";
+			return "is_not_null";
 		case IS_NULL:
 			return "is_null";
 		case LESS_THAN:
diff --git a/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchQuery.java b/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchQuery.java
index 6be838a..dc05bf8 100644
--- a/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchQuery.java
+++ b/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchQuery.java
@@ -94,6 +94,8 @@
 	 */
 	List<Value> getFilterValues(Attribute attribute, Filter filter) throws DataAccessException;
 
+	List<Result> getFilterResults(List<Attribute> attributes, Filter filter) throws DataAccessException;
+
 	/**
 	 * Executes this search query with given {@link EntityType}s. The {@code
 	 * EntityType}s must be fully supported by this search query. This method
diff --git a/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchService.java b/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchService.java
index 4ea388e..5223f54 100644
--- a/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchService.java
+++ b/api/base/src/main/java/org/eclipse/mdm/api/base/search/SearchService.java
@@ -126,6 +126,8 @@
 	List<Value> getFilterValues(Class<? extends Entity> entityClass, Attribute attribute, Filter filter)
 			throws DataAccessException;
 
+	List<Result> getFilterResults(Class<? extends Entity> entityClass, List<Attribute> attributes, Filter filter) throws DataAccessException;
+
 	/**
 	 * Executes the associated {@link SearchQuery} with given {@link EntityType}s.
 	 * The {@code EntityType}s must be fully supported by the {@code SearchQuery}
diff --git a/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/BaseEntitySearchQuery.java b/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/BaseEntitySearchQuery.java
index d3a34b7..d143f95 100644
--- a/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/BaseEntitySearchQuery.java
+++ b/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/BaseEntitySearchQuery.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.reducing;
 
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -157,6 +158,28 @@
 	 * {@inheritDoc}
 	 */
 	@Override
+	public List<Result> getFilterResults(List<Attribute> attributes, Filter filter) throws DataAccessException {
+		Query query = queryService.createQuery();
+		for (Attribute attribute : attributes) {
+			addJoins(query, attribute.getEntityType());
+			query.select(attribute, Aggregation.DISTINCT);
+			query.group(attribute);
+		}
+
+		// add required joins
+		filter.stream().filter(FilterItem::isCondition).map(FilterItem::getCondition).forEach(c -> {
+			addJoins(query, c.getAttribute().getEntityType());
+		});
+
+		addImplicitLocalColumnJoin(query);
+
+		return query.fetch(filter);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
 	public List<Result> fetchComplete(List<EntityType> entityTypes, Filter filter) throws DataAccessException {
 		Query query = queryService.createQuery().selectID(modelManager.getEntityType(entityClass));
 
@@ -298,7 +321,8 @@
 	 * @param entityType The target {@link EntityType}.
 	 */
 	private void addJoins(Query query, EntityType entityType) {
-		if (query.isQueried(entityType)) {
+		EntityType searchQueryEntityType = modelManager.getEntityType(entityClass);
+		if (query.isQueried(entityType) || entityType.equals(searchQueryEntityType)) {
 			return;
 		}
 
diff --git a/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/MergedSearchQuery.java b/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/MergedSearchQuery.java
index 4ace2fb..3b11123 100644
--- a/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/MergedSearchQuery.java
+++ b/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/MergedSearchQuery.java
@@ -17,8 +17,10 @@
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.reducing;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -30,6 +32,9 @@
 import org.eclipse.mdm.api.base.adapter.Attribute;
 import org.eclipse.mdm.api.base.adapter.EntityType;
 import org.eclipse.mdm.api.base.model.Value;
+import org.eclipse.mdm.api.base.query.Aggregation;
+import org.eclipse.mdm.api.base.query.ComparisonOperator;
+import org.eclipse.mdm.api.base.query.Condition;
 import org.eclipse.mdm.api.base.query.DataAccessException;
 import org.eclipse.mdm.api.base.query.Filter;
 import org.eclipse.mdm.api.base.query.Record;
@@ -97,7 +102,7 @@
 
 		return Stream.concat(orderValues.stream(), resultValues.stream())
 				// group by value and merge values
-				.collect(groupingBy(v -> v.extract(), reducing((v1, v2) -> v1)))
+				.collect(groupingBy(Value::extract, reducing((v1, v2) -> v1)))
 				// collect merged results
 				.values().stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
 	}
@@ -106,6 +111,71 @@
 	 * {@inheritDoc}
 	 */
 	@Override
+	public List<Result> getFilterResults(List<Attribute> attributes, Filter filter)
+			throws DataAccessException {
+		ContextState contextState = filter.getContext();
+		List<Result> firstResults;
+		List<Result> secondResults;
+
+		if (ContextState.ORDERED.equals(contextState)) {
+			firstResults = byOrder.getFilterResults(attributes, filter);
+		} else {
+			firstResults = byResult.getFilterResults(attributes, filter);
+		}
+
+		if (firstResults.isEmpty()) {
+			return Collections.emptyList();
+		}
+
+		Filter followUpFilter = Filter.or();
+		for (Result result : firstResults) {
+			Filter attributeValueFilter = Filter.or();
+			for (Attribute attr : attributes) {
+				Value value = result.getValue(attr, Aggregation.DISTINCT);
+				attributeValueFilter.add(ComparisonOperator.EQUAL.create(contextState, attr, value.extract(contextState)));
+			}
+			followUpFilter.merge(attributeValueFilter);
+		}
+
+		if (ContextState.ORDERED.equals(contextState)) {
+			secondResults = byResult.getFilterResults(attributes, followUpFilter);
+		} else {
+			secondResults = byOrder.getFilterResults(attributes, followUpFilter);
+		}
+
+		Set<EntityType> firstTypes = getTypesFromResults(firstResults);
+
+		Set<EntityType> secondTypes = getTypesFromResults(secondResults);
+
+
+		Set<EntityType> types = new HashSet<>(firstTypes);
+		types.addAll(secondTypes);
+
+		if (types.size() > 1) {
+			throw new DataAccessException("Cannot handle multiple types here");
+		}
+
+		return Stream.concat(firstResults.stream(), secondResults.stream())
+				.collect(groupingBy(r -> getAttributeValues(attributes, r, Aggregation.DISTINCT), reducing(Result::merge)))
+				.values().stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+	}
+
+	private Set<EntityType> getTypesFromResults(List<Result> results) {
+		return results.stream()
+			.map(result -> result.stream().collect(Collectors.toList()))
+			.flatMap(Collection::stream)
+			.map(Record::getEntityType)
+			.collect(Collectors.toSet());
+	}
+
+	private String getAttributeValues(List<Attribute> attributes, Result result, Aggregation aggregation) {
+		return attributes.stream().map(attribute -> result.getValue(attribute, aggregation).extract()).map(String::valueOf).collect(Collectors.joining("_"));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
 	public List<Result> fetchComplete(List<EntityType> entityTypes, Filter filter) throws DataAccessException {
 		return mergeResults(byOrder.fetchComplete(entityTypes, filter), byResult.fetchComplete(entityTypes, filter));
 	}
@@ -229,9 +299,13 @@
 	 * @return The merged {@link Result} is returned.
 	 */
 	private List<Result> mergeResults(List<Result> results1, List<Result> results2) {
+		return mergeResults(entityType, Aggregation.NONE, results1, results2);
+	}
+
+	private List<Result> mergeResults(EntityType mergeType, Aggregation aggregation, List<Result> results1, List<Result> results2) {
 		return Stream.concat(results1.stream(), results2.stream())
 				// group by instance ID and merge grouped results
-				.collect(groupingBy(r -> getMergeId(r.getRecord(entityType)), reducing(Result::merge)))
+				.collect(groupingBy(r -> getMergeId(mergeType, r.getRecord(mergeType), aggregation), reducing(Result::merge)))
 				// collect merged results
 				.values().stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
 	}
@@ -244,12 +318,23 @@
 	 * @return Returns the ID of the record, if ID of ContextState.MEASURED is not
 	 *         set, returns ID of ContextState.ORDERED.
 	 */
-	private String getMergeId(Record rec) {
-		if (rec.getID() == null) {
-			// MEASURED (default) ContextState is null, so we use the ID stored in ORDERED
-			return rec.getValues().get(entityType.getIDAttribute().getName()).extract(ContextState.ORDERED);
+	private String getMergeId(EntityType idType, Record rec, Aggregation aggregation) {
+
+		String idKey;
+		if (Aggregation.NONE == aggregation) {
+			idKey = idType.getIDAttribute().getName();
 		} else {
-			return rec.getID();
+			idKey = String.format("%s(%s)", aggregation.name(), idType.getIDAttribute().getName());
+		}
+
+		Value idValue = rec.getValues().get(idKey);
+		String id =  idValue.extract();
+
+		if (id == null) {
+			// MEASURED (default) ContextState is null, so we use the ID stored in ORDERED
+			return rec.getValues().get(idKey).extract(ContextState.ORDERED);
+		} else {
+			return id;
 		}
 
 	}
diff --git a/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/ODSSearchService.java b/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/ODSSearchService.java
index 2725cbf..8757ed8 100644
--- a/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/ODSSearchService.java
+++ b/api/odsadapter/src/main/java/org/eclipse/mdm/api/odsadapter/search/ODSSearchService.java
@@ -124,6 +124,13 @@
 		return findSearchQuery(entityClass).getFilterValues(attribute, filter);
 	}
 
+	@Override
+	public List<Result> getFilterResults(final Class<? extends Entity> entityClass,
+	                                               final List<Attribute> attributes,
+	                                               final Filter filter) throws DataAccessException {
+		return findSearchQuery(entityClass).getFilterResults(attributes, filter);
+	}
+
 	/**
 	 * {@inheritDoc}
 	 */
@@ -155,7 +162,9 @@
 
 		EntityType entityType = context.getODSModelManager().getEntityType(entityClass);
 		Map<String, Result> recordsByEntityID = new HashMap<>();
-		for (Result result : findSearchQuery(entityClass).fetch(attributes, mergedFilter)) {
+		SearchQuery searchQuery = findSearchQuery(entityClass);
+		List<Result> results = searchQuery.fetch(attributes, mergedFilter);
+		for (Result result : results) {
 			recordsByEntityID.put(result.getRecord(entityType).getID(), result);
 		}
 
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/businessobjects/control/FilterParser.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/businessobjects/control/FilterParser.java
index b52d6aa..687cc17 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/businessobjects/control/FilterParser.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/businessobjects/control/FilterParser.java
@@ -14,12 +14,15 @@
 
 package org.eclipse.mdm.businessobjects.control;
 
+import static java.util.stream.Collectors.joining;
+
 import java.time.LocalDateTime;
 import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.antlr.v4.runtime.ANTLRInputStream;
 import org.antlr.v4.runtime.BaseErrorListener;
@@ -254,10 +257,9 @@
 			final Predicate<EntityType> filterByEntityTypeName = entityType -> ServiceUtils
 					.workaroundForTypeMapping(entityType).equals(typeName);
 
-			return availableEntityTypes.stream()
-					.filter(filterByEntityTypeName)
-					.findAny()
-					.orElseThrow(() -> new IllegalArgumentException("Entity " + typeName + " not found in data source!"))
+			return availableEntityTypes.stream().filter(filterByEntityTypeName).findAny()
+					.orElseThrow(
+							() -> new IllegalArgumentException("Entity " + typeName + " not found in data source!"))
 					.getAttribute(attributeName);
 		}
 
@@ -483,8 +485,10 @@
 	private static String toString(Condition condition) {
 		StringBuilder builder = new StringBuilder();
 
-		if (condition.getContextState() != null && condition.getContextState().isOrdered()) {
-			builder.append("ordered.");
+		ContextState contextState = condition.getContextState();
+		if (contextState != null) {
+			builder.append(contextState.toString());
+			builder.append(".");
 		}
 
 		builder.append(ServiceUtils.workaroundForTypeMapping(condition.getAttribute().getEntityType()));
@@ -494,10 +498,17 @@
 		builder.append(condition.getComparisonOperator());
 		builder.append(" ");
 
-		if (condition.getValue().getValueType().isNumericalType()) {
-			builder.append("" + condition.getValue().extract());
-		} else {
-			builder.append("\"" + condition.getValue().extract() + "\"");
+		boolean isValid = condition.getValue().isValid();
+
+		if (isValid) {
+			if (condition.getValue().getValueType().isNumericalType()) {
+				builder.append("" + condition.getValue().extract());
+			} else if (condition.getValue().getValueType().isSequence()) {
+				builder.append(Stream.of(condition.getValue().extract()).map(Object::toString)
+						.collect(joining("', '", "('", "')")));
+			} else {
+				builder.append("\"" + condition.getValue().extract() + "\"");
+			}
 		}
 		return builder.toString();
 	}
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/AttributeContainer.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/AttributeContainer.java
new file mode 100644
index 0000000..07c8e74
--- /dev/null
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/AttributeContainer.java
@@ -0,0 +1,36 @@
+package org.eclipse.mdm.nodeprovider.control;
+
+import org.eclipse.mdm.api.base.adapter.Attribute;
+import org.eclipse.mdm.api.base.search.ContextState;
+
+public class AttributeContainer {
+
+    private ContextState contextState;
+    private Attribute attribute;
+    private boolean valid;
+    private Object value;
+
+    public AttributeContainer(final ContextState contextState, final Attribute attribute, final boolean valid,
+                              final Object value) {
+        this.contextState = contextState;
+        this.attribute = attribute;
+        this.valid = valid;
+        this.value = value;
+    }
+
+    public ContextState getContextState() {
+        return contextState;
+    }
+
+    public Attribute getAttribute() {
+        return attribute;
+    }
+
+    public boolean isValid() {
+        return valid;
+    }
+
+    public Object getValue() {
+        return value;
+    }
+}
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/DefaultNodeProvider.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/DefaultNodeProvider.java
index 7062bfd..7c93adb 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/DefaultNodeProvider.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/DefaultNodeProvider.java
@@ -1,3 +1,17 @@
+/********************************************************************************
+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
 package org.eclipse.mdm.nodeprovider.control;
 
 import java.util.ArrayList;
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProvider.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProvider.java
index 75ad230..6527355 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProvider.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProvider.java
@@ -1,10 +1,27 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.control;

 

+import static java.util.stream.Collectors.toMap;

+

 import java.util.ArrayList;

-import java.util.Arrays;

 import java.util.Collections;

 import java.util.Comparator;

+import java.util.HashSet;

 import java.util.List;

+import java.util.Set;

 import java.util.stream.Collectors;

 

 import org.eclipse.mdm.api.base.ServiceNotProvidedException;

@@ -21,6 +38,7 @@
 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.Aggregation;

 import org.eclipse.mdm.api.base.query.BooleanOperator;

 import org.eclipse.mdm.api.base.query.ComparisonOperator;

 import org.eclipse.mdm.api.base.query.Filter;

@@ -34,13 +52,13 @@
 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.entity.ValuePrecision;

 import org.eclipse.mdm.nodeprovider.utils.SerializationUtil;

 import org.eclipse.mdm.protobuf.Mdm.Node;

 

@@ -57,6 +75,8 @@
 	private ConnectorService connectorService;

 

 	private NodeProviderRoot root;

+	

+	private MDMExpressionLanguageService elService;

 

 	/**

 	 * Construct a new {@link GenericNodeProvider} using the given

@@ -65,9 +85,10 @@
 	 * @param connectorService

 	 * @param root

 	 */

-	public GenericNodeProvider(ConnectorService connectorService, NodeProviderRoot root) {

+	public GenericNodeProvider(ConnectorService connectorService, NodeProviderRoot root, MDMExpressionLanguageService expressionLanguageService) {

 		this.connectorService = connectorService;

 		this.root = root;

+		this.elService = expressionLanguageService;

 	}

 

 	/**

@@ -111,17 +132,14 @@
 	@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()));

+			nodes.add(convertNode(r, node.getSource(), nodeLevel, Filter.and(), Aggregation.NONE));

 		}

 

 		if (nodes.size() != 1) {

@@ -153,7 +171,7 @@
 

 		if (isEnvironment(childNodeLevel.getEntityType())) {

 			return createQueryForNodeLevel(context, childNodeLevel).fetch().stream()

-					.map(r -> convertNode(r, sourceName, childNodeLevel, filter)).collect(Collectors.toList());

+					.map(result -> convertNode(result, sourceName, childNodeLevel, filter, Aggregation.NONE)).collect(Collectors.toList());

 		}

 

 		if (!sourceName.equalsIgnoreCase(parent.getSource()) || !sourceNameMatches(filter, sourceName)) {

@@ -162,15 +180,15 @@
 

 		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());

-

+			List<Result> results = getSearchService(context).getFilterResults(TestStep.class, childNodeLevel.getLabelAttributes(), filter);

+			

 			// Filter values are always in measured

-			List<Node> nodes = new ArrayList<>();

-			for (Value v : values) {

-				nodes.add(convertNode(v, sourceName, childNodeLevel, filter));

-			}

-			return nodes;

+			return results.stream()

+					.map(result -> convertLabelAttributesToContainers(result, Aggregation.DISTINCT, childNodeLevel))

+					.collect(toMap(v -> createBasicLabel(childNodeLevel.getContextState(), v), v -> v, (v1, v2) -> v1))

+					.values().stream()

+					.map(containers -> convertNodeByLabel(containers, sourceName, childNodeLevel, filter))

+					.collect(Collectors.toList());

 		} else {

 			return fetchNodes(context, childNodeLevel, filter);

 		}

@@ -201,7 +219,7 @@
 			Query query = createQueryForNodeLevel(context, parentLevel);

 

 			for (Result r : query.fetch()) {

-				return convertNode(r, context.getSourceName(), parentLevel, childFilter);

+				return convertNode(r, context.getSourceName(), parentLevel, childFilter, Aggregation.NONE);

 			}

 			return null;

 		}

@@ -209,12 +227,10 @@
 		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, "");

+			List<Result> results = getSearchService(context).fetchResults(TestStep.class, parentLevel.getLabelAttributes(), childFilter, "");

 

 			for (Result r : results) {

-				Value value = r.getValue(parentLevel.getLabelAttribute());

-				nodes.add(convertNode(value, node.getSource(), parentLevel, childFilter));

+				nodes.add(convertNode(r, node.getSource(), parentLevel, childFilter, Aggregation.NONE));

 				break;

 			}

 		} else {

@@ -238,7 +254,7 @@
 	private Filter createFilterFromIdOrLabel(Node node, NodeLevel nodeLevel) {

 		if (nodeLevel.isVirtual()) {

 			return Filter.and().add(ComparisonOperator.EQUAL.create(nodeLevel.getContextState(),

-					nodeLevel.getLabelAttribute(), node.getLabel()));

+					getLabelAttribute(nodeLevel), node.getLabel()));

 		} else {

 			return Filter.and().id(nodeLevel.getEntityType(), node.getId());

 		}

@@ -281,12 +297,12 @@
 	 */

 	private List<Node> fetchNodes(ApplicationContext context, NodeLevel nodeLevel, Filter filter) {

 

-		Class<? extends Entity> entityClass = getEntityClass(nodeLevel.getIdAttribute().getEntityType());

+		Class<? extends Entity> entityClass = getEntityClass(getFilterAttribute(nodeLevel).getEntityType());

 

-		List<Result> results = getSearchService(context).fetchResults(entityClass,

-				Arrays.asList(nodeLevel.getIdAttribute(), nodeLevel.getLabelAttribute()), filter, "");

+		List<Attribute> attributes = getAttributesFromNodeLevel(nodeLevel);

+		List<Result> results = getSearchService(context).fetchResults(entityClass, attributes, filter, "");

 

-		return results.stream().map(r -> convertNode(r, context.getSourceName(), nodeLevel, filter))

+		return results.stream().map(r -> convertNode(r, context.getSourceName(), nodeLevel, filter, Aggregation.NONE))

 				.collect(Collectors.toList());

 	}

 

@@ -318,7 +334,7 @@
 		case "Quantity":

 			return Quantity.class;

 		default:

-			throw new RuntimeException("Could not find entity class for entity with name '" + entityName + "'.");

+			throw new NodeProviderException("Could not find entity class for entity with name '" + entityName + "'.");

 		}

 	}

 

@@ -373,61 +389,108 @@
 	/**

 	 * Converts a Result to a Node

 	 * 

-	 * @param r            Result to convert

+	 * @param result            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());

+	private Node convertNode(Result result, String sourceName, NodeLevel nodeLevel, Filter parentFilter, Aggregation aggregation) {

+		

+		ContextState contextState = nodeLevel.getContextState();

 

-		Filter newFilter = null;

+		List<AttributeContainer> labelAttrContainers = convertToAttributeContainers(contextState, result, aggregation, nodeLevel.getLabelAttributes(), nodeLevel.getValuePrecision());

 

-		if (parentFilter != null) {

-			if (isEnvironment(nodeLevel.getEntityType())) {

-				newFilter = parentFilter.copy();

-			} else {

-				newFilter = parentFilter.copy().add(

-						ComparisonOperator.EQUAL.create(nodeLevel.getContextState(), nodeLevel.getIdAttribute(), id));

-			}

-		}

+		String label = createLabel(nodeLevel, labelAttrContainers);

 

-		return SerializationUtil.createNode(sourceName,

-				ServiceUtils.workaroundForTypeMapping(nodeLevel.getEntityType()), id,

-				nodeLevel.getIdAttribute().getName(), newFilter, label);

+		Value idValue = result.getValue(nodeLevel.getEntityType().getIDAttribute(), aggregation);

+		String id = idValue.extract();

+

+		List<AttributeContainer> filterAttrContainers = convertToAttributeContainers(contextState, result, aggregation, nodeLevel.getFilterAttributes(), nodeLevel.getValuePrecision());

+		Filter newFilter = getNewNodeFilter(contextState, nodeLevel, parentFilter, filterAttrContainers);

+

+		return convertNode(id, label, sourceName, nodeLevel.getEntityType(), getFilterAttribute(nodeLevel).getName(), newFilter);

 	}

 

 	/**

-	 * Converts a Value to a Node.

-	 * 

-	 * @param v            value to convert

+	 * Converts a Result to a Node

+	 *

+	 * @param result            Result to convert

 	 * @param sourceName   name of the source

 	 * @param nodeLevel    NodeLevel

 	 * @param parentFilter Filter of the parent Node

-	 * @return Value converted to a Node

+	 * @return Result converted to a Node

 	 */

-	private Node convertNode(Value v, String sourceName, NodeLevel nodeLevel, Filter parentFilter) {

+	private Node convertNodeByLabel(List<AttributeContainer> labelAttrContainers, String sourceName, NodeLevel nodeLevel, Filter parentFilter) {

+		ContextState contextState = nodeLevel.getContextState();

 

-		ContextState contextState = nodeLevel.getContextState() == null ? ContextState.MEASURED

-				: nodeLevel.getContextState();

+		String label = createLabel(nodeLevel, labelAttrContainers);

 

-		String label = Serializer.serializeValue(v).toString();

+		/**

+		 * TODO jst, 05.02.2021: This is counterintuitive. LabelAttributes are used for filtering..

+		 */

+		Filter newFilter = getNewNodeFilter(contextState, nodeLevel, parentFilter, labelAttrContainers);

 

-		Filter newFilter = null;

+		return convertNode(null, label, sourceName, nodeLevel.getEntityType(), getFilterAttribute(nodeLevel).getName(), newFilter);

+	}

 

-		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()));

+	/**

+	 * Creates label for {@link NodeLevel} respecting the NodeLevels {@link ValueExpression}.

+	 *

+	 * @param nodeLevel				the {@link NodeLevel}

+	 * @param attributeContainers	the label attributes

+	 * @return

+	 */

+	private String createLabel(NodeLevel nodeLevel, List<AttributeContainer> attributeContainers) {

+		String label;

+		if (nodeLevel.getLabelExpression() == null) {

+			label = createBasicLabel(nodeLevel.getContextState(), attributeContainers);

+		} else {

+			label = elService.evaluateValueExpression(nodeLevel.getLabelExpression(), attributeContainers);

 		}

+		return label;

+	}

 

-		return SerializationUtil.createNode(sourceName,

-				ServiceUtils.workaroundForTypeMapping(nodeLevel.getEntityType()), label,

-				nodeLevel.getIdAttribute().getName(), newFilter, label);

+	/**

+	 * Creates a basic label for {@link NodeLevel} of label attributes, while *ignoring* the NodeLevels {@link ValueExpression}.

+	 * 

+	 * @param contextState

+	 * @param attributeContainers	the label attributes

+	 * @return

+	 */

+	private String createBasicLabel(ContextState contextState, List<AttributeContainer> attributeContainers) {

+		String labelValue = attributeContainers.stream()

+				.map(this::mapAttributeContainerValue)

+				.collect(Collectors.joining(" "));

+

+		if (contextState == null) {

+			return labelValue;

+		} else {

+			return String.format("%s %s", contextState, labelValue);

+		}

+	}

+

+	private List<AttributeContainer> convertLabelAttributesToContainers(Result result, Aggregation aggregation, NodeLevel nodeLevel) {

+		return convertToAttributeContainers(nodeLevel.getContextState(), result, aggregation, nodeLevel.getLabelAttributes(), nodeLevel.getValuePrecision());

+	}

+

+	private List<AttributeContainer> convertToAttributeContainers(ContextState contextState, Result result, Aggregation aggregation, List<Attribute> attributes, ValuePrecision precision) {

+		return attributes.stream().map(attribute -> {

+			Value value = result.getValue(attribute, aggregation);			

+			precision.applyOn(result.getValue(attribute, aggregation));

+			Object extractedValue = value.extract(contextState);

+			boolean valid = value.isValid(contextState);

+			return new AttributeContainer(contextState, attribute, valid, extractedValue);

+		}).collect(Collectors.toList());

+	}

+

+	private String mapAttributeContainerValue(AttributeContainer attributeContainer) {

+		if (attributeContainer.isValid()) {

+			String valueAsString = String.valueOf(attributeContainer.getValue());

+			return String.format("%s", valueAsString);

+		} else {

+			return String.format("%s=invalid", attributeContainer.getAttribute().getName());

+		}

 	}

 

 	/**

@@ -438,15 +501,50 @@
 	 * @return Query for the given NodeLevel

 	 */

 	private Query createQueryForNodeLevel(ApplicationContext context, NodeLevel nodeLevel) {

+		List<Attribute> attributes = getAttributesFromNodeLevel(nodeLevel);

 		Query query = context.getQueryService().orElseThrow(() -> new ServiceNotProvidedException(QueryService.class))

-				.createQuery().select(nodeLevel.getIdAttribute(), nodeLevel.getLabelAttribute())

-				.group(nodeLevel.getIdAttribute(), nodeLevel.getLabelAttribute());

+				.createQuery()

+				.select(attributes)

+				.group(attributes);

 

-		nodeLevel.getOrderAttributes().stream()

-				.forEach(oa -> query.order(oa.getAttribute(), oa.getOrder() == Order.ASCENDING));

+		nodeLevel.getOrderAttributes().forEach(oa -> query.order(oa.getAttribute(), oa.getOrder() == Order.ASCENDING));

 

 		return query;

 	}

+	

+	private List<Attribute> getAttributesFromNodeLevel(NodeLevel nodeLevel) {

+		Set<Attribute> attributes = new HashSet<>();

+		attributes.addAll(nodeLevel.getFilterAttributes());

+		attributes.addAll(nodeLevel.getLabelAttributes());

+		

+		return new ArrayList<>(attributes);

+	}

+

+	private Node convertNode(String id, String label, String sourceName, EntityType entityType, String idAttr, Filter newFilter) {

+		return SerializationUtil.createNode(sourceName,

+				ServiceUtils.workaroundForTypeMapping(entityType), id,

+				idAttr, newFilter, label);

+	}

+

+	private Filter getNewNodeFilter(ContextState contextState, NodeLevel nodeLevel, Filter parentFilter, List<AttributeContainer> attributeContainers) {

+		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();

+

+			for (AttributeContainer attributeContainer : attributeContainers) {

+				if (attributeContainer.isValid()) {

+					newFilter.add(nodeLevel.getValuePrecision().getCondition(contextState, attributeContainer.getAttribute(), attributeContainer.getValue()));

+				} else {

+					newFilter.add(ComparisonOperator.IS_NULL.create(contextState, attributeContainer.getAttribute(), null));

+				}

+			}

+		}

+		return newFilter;

+	}

 

 	/**

 	 * Returns the SearchService

@@ -470,14 +568,17 @@
 		return Environment.class.getSimpleName().equals(entityType.getName());

 	}

 

+	private Attribute getFilterAttribute(NodeLevel nodeLevel) {

+		return nodeLevel.getFilterAttributes().stream().findFirst().orElseThrow(() -> new IllegalStateException("woops?!"));

+	}

+

+	private Attribute getLabelAttribute(NodeLevel nodeLevel) {

+		return nodeLevel.getLabelAttributes().stream().findFirst().orElseThrow(() -> new IllegalStateException("woops?!"));

+	}

+

 	/**

 	 * 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());

-		}

-	};

+	public static final Comparator<Node> compareByLabel = Comparator.comparing(Node::getLabel);

 

 }

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/MDMExpressionLanguageService.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/MDMExpressionLanguageService.java
new file mode 100644
index 0000000..d0b5fd5
--- /dev/null
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/MDMExpressionLanguageService.java
@@ -0,0 +1,93 @@
+/********************************************************************************
+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+package org.eclipse.mdm.nodeprovider.control;
+
+import java.lang.reflect.Method;
+
+import javax.el.ELManager;
+import javax.el.ELProcessor;
+import javax.el.ExpressionFactory;
+import javax.el.StandardELContext;
+import javax.el.ValueExpression;
+
+import org.antlr.v4.tool.Attribute;
+import org.eclipse.mdm.nodeprovider.entity.NodeLevel;
+import org.eclipse.mdm.nodeprovider.entity.AttributeContainerListResovler;
+import org.eclipse.mdm.nodeprovider.utils.ExpressionLanguageMethodProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MDMExpressionLanguageService {
+
+	private static final Logger LOG = LoggerFactory.getLogger(MDMExpressionLanguageService.class);
+
+	private ELProcessor processor;
+	private ExpressionFactory factory;
+	
+	/**
+	 * Intentionally empty
+	 */
+	public MDMExpressionLanguageService() {
+		processor = new ELProcessor();
+		factory = ELManager.getExpressionFactory();
+		init();
+	}
+
+	/**
+	 * Initialize functionMapper and resolvers for expression language interpretation.
+	 */
+	public void init() {
+		ELManager manager = processor.getELManager();
+		manager.addELResolver(new AttributeContainerListResovler());
+
+		for(Method method : ExpressionLanguageMethodProvider.class.getMethods()) {
+			try {
+				processor.defineFunction("fn", "", method);
+			} catch (NoSuchMethodException e) {
+				LOG.warn("Could not add method: " + method.toString());
+			}
+		};
+	}
+
+	/**
+	 * Parses {@link ValueExpression} of given {@link NodeLevel} if present.
+	 *
+	 * @param nodeLevel		the node level
+	 * @return				the value expression, or null if none present
+	 */
+	public ValueExpression parseValueExpression(String expressionString) {
+		StandardELContext elContext = processor.getELManager().getELContext();
+		ValueExpression valueExpression = null;
+		if (expressionString != null) {			
+			valueExpression = factory.createValueExpression(elContext, expressionString, String.class);
+		}
+		return valueExpression;
+	}
+
+	/**
+	 * Evaluates the {@link ValueExpression} with respect to given properties.
+	 * 
+	 * @param valueExpression	the value expression
+	 * @param properties		the properties to be inserted
+	 * @return
+	 */
+	public String evaluateValueExpression(ValueExpression valueExpression, Object ...properties) {
+		StandardELContext context = processor.getELManager().getELContext();
+		for (Object prop : properties) {
+			context.putContext(prop.getClass(), prop);
+		}
+		return valueExpression.getValue(context).toString();
+	}
+}
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderException.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderException.java
index f182944..0a258ee 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderException.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderException.java
@@ -1,3 +1,17 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.control;

 

 /**

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderRepository.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderRepository.java
index 4fbd8ff..29ef1d8 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderRepository.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/control/NodeProviderRepository.java
@@ -24,7 +24,6 @@
 import javax.enterprise.context.SessionScoped;

 import javax.inject.Inject;

 

-import org.eclipse.mdm.businessobjects.boundary.ChannelResource;

 import org.eclipse.mdm.connector.boundary.ConnectorService;

 import org.eclipse.mdm.nodeprovider.entity.NodeProvider;

 import org.eclipse.mdm.nodeprovider.entity.NodeProviderRoot;

@@ -43,7 +42,7 @@
 

 	private static final long serialVersionUID = -3081666937067348673L;

 

-	private static final Logger LOG = LoggerFactory.getLogger(ChannelResource.class);

+	private static final Logger LOG = LoggerFactory.getLogger(NodeProviderRepository.class);

 

 	private Map<String, NodeProvider> nodeProviders = new HashMap<>();

 

@@ -85,12 +84,14 @@
 

 		putNodeProvider("default", defaultNP);

 		LOG.trace("Registered default nodeprovider.");

+		

+		MDMExpressionLanguageService expressionLanguageService = new MDMExpressionLanguageService();

 

 		List<PreferenceMessage> msgs = preferenceService.getPreferences("system", "nodeprovider.");

 		for (PreferenceMessage msg : msgs) {

 			try {

 				NodeProviderRoot root = parsePreference(msg.getValue());

-				putNodeProvider(root.getId(), new GenericNodeProvider(connectorService, root));

+				putNodeProvider(root.getId(), new GenericNodeProvider(connectorService, root, expressionLanguageService));

 				LOG.trace("Registered generic nodeprovider '{}'.", root.getId());

 			} catch (RuntimeException e) {

 				e.printStackTrace();

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/AttributeContainerListResovler.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/AttributeContainerListResovler.java
new file mode 100644
index 0000000..bebe8c1
--- /dev/null
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/AttributeContainerListResovler.java
@@ -0,0 +1,76 @@
+/********************************************************************************
+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+package org.eclipse.mdm.nodeprovider.entity;
+
+import java.beans.FeatureDescriptor;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Optional;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+
+import org.eclipse.mdm.nodeprovider.control.AttributeContainer;
+
+public class AttributeContainerListResovler extends ELResolver {
+
+	@Override
+	public void setValue(ELContext context, Object base, Object property, Object value) {
+		//  Intentionally empty.
+	}
+	
+	@Override
+	public boolean isReadOnly(ELContext context, Object base, Object property) {
+		return true;
+	}
+	
+	@Override
+	public Object getValue(ELContext context, Object base, Object property) {
+		Optional<Object> resolved = getValue(context, property);
+		if (resolved.isPresent()) {			
+			context.setPropertyResolved(base, property);
+		}
+		return resolved.orElseGet(null);
+	}
+	
+	@Override
+	public Class<?> getType(ELContext context, Object base, Object property) {
+		Optional<Object> resolved = getValue(context, property);
+		if (resolved.isPresent()) {			
+			return resolved.get().getClass();
+		} else {
+			return null;
+		}
+	}
+
+	private Optional<Object> getValue(ELContext context, Object property) {
+		ArrayList<AttributeContainer> labelAttrContainers = (ArrayList<AttributeContainer>) context.getContext(ArrayList.class);
+		return labelAttrContainers.stream()
+				.filter(attr -> attr.getAttribute().getName().equalsIgnoreCase(property.toString()))
+				.map(AttributeContainer::getValue).findFirst();
+	}
+	
+	@Override
+	public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+	
+	@Override
+	public Class<?> getCommonPropertyType(ELContext context, Object base) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+}
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeLevel.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeLevel.java
index 9d5aca4..a407b22 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeLevel.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeLevel.java
@@ -1,9 +1,27 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.entity;

 

+import static java.util.stream.Collectors.toList;

+

 import java.util.Arrays;

 import java.util.Collections;

 import java.util.List;

 

+import javax.el.ValueExpression;

+

 import org.eclipse.mdm.api.base.adapter.Attribute;

 import org.eclipse.mdm.api.base.adapter.EntityType;

 import org.eclipse.mdm.api.base.search.ContextState;

@@ -25,8 +43,10 @@
 

 	private EntityType type;

 

-	private Attribute idAttribute;

-	private Attribute labelAttribute;

+	private List<Attribute> filterAttributes;

+	private List<Attribute> labelAttributes;

+	private ValueExpression labelExpression;

+	private ValuePrecision valuePrecision;

 	private List<SortAttribute> orderAttributes;

 

 	private ContextState contextState = null;

@@ -50,11 +70,37 @@
 	 * label attributes.

 	 * 

 	 * @param type

-	 * @param idAttribute

+	 * @param filterAttribute

 	 * @param labelAttribute

 	 */

-	public NodeLevel(EntityType type, Attribute idAttribute, Attribute labelAttribute) {

-		this(type, idAttribute, labelAttribute, Arrays.asList(new SortAttribute(labelAttribute)));

+	public NodeLevel(EntityType type, Attribute filterAttribute, Attribute labelAttribute) {

+		this(type, filterAttribute, labelAttribute, Arrays.asList(new SortAttribute(labelAttribute)));

+	}

+

+	/**

+	 * Constructs NodeLevel for the given {@link EntityType} using the given ID and

+	 * label attributes with the given list of order attributes.

+	 *

+	 * @param type

+	 * @param filterAttribute

+	 * @param labelAttribute

+	 * @param orderAttributes

+	 */

+	public NodeLevel(EntityType type, Attribute filterAttribute, Attribute labelAttribute,

+	                 List<SortAttribute> orderAttributes) {

+		this(type, Arrays.asList(filterAttribute), Arrays.asList(labelAttribute), orderAttributes);

+	}

+

+	/**

+	 * Constructs NodeLevel for the given {@link EntityType} using the given ID and

+	 * label attributes with the given list of order attributes.

+	 *

+	 * @param type

+	 * @param filterAttributes

+	 * @param labelAttributes

+	 */

+	public NodeLevel(EntityType type, List<Attribute> filterAttributes, List<Attribute> labelAttributes) {

+		this(type, filterAttributes, labelAttributes, labelAttributes.stream().map(SortAttribute::new).collect(toList()));

 	}

 

 	/**

@@ -62,16 +108,17 @@
 	 * label attributes with the given list of order attributes.

 	 * 

 	 * @param type

-	 * @param idAttribute

+	 * @param filterAttribute

 	 * @param labelAttribute

 	 * @param orderAttributes

 	 */

-	public NodeLevel(EntityType type, Attribute idAttribute, Attribute labelAttribute,

-			List<SortAttribute> orderAttributes) {

+	public NodeLevel(EntityType type, List<Attribute> filterAttribute, List<Attribute> labelAttribute,

+	                 List<SortAttribute> orderAttributes) {

 		this.type = type;

-		this.idAttribute = idAttribute;

-		this.labelAttribute = labelAttribute;

+		this.filterAttributes = filterAttribute;

+		this.labelAttributes = labelAttribute;

 		this.orderAttributes = Lists.newArrayList(orderAttributes);

+		this.valuePrecision = ValuePrecision.EXACT;

 	}

 

 	/**

@@ -119,15 +166,15 @@
 	/**

 	 * @return the ID attribute

 	 */

-	public Attribute getIdAttribute() {

-		return idAttribute;

+	public List<Attribute> getFilterAttributes() {

+		return filterAttributes;

 	}

 

 	/**

 	 * @return the label attribute

 	 */

-	public Attribute getLabelAttribute() {

-		return labelAttribute;

+	public List<Attribute> getLabelAttributes() {

+		return labelAttributes;

 	}

 

 	/**

@@ -158,12 +205,42 @@
 	public String toString() {

 		return MoreObjects.toStringHelper(NodeLevel.class)

 				.add("type", type)

-				.add("idAttribute", idAttribute)

-				.add("labelAttribute", labelAttribute)

+				.add("filterAttributes", filterAttributes)

+				.add("labelAttributes", labelAttributes)

+				.add("labelExpression", labelExpression)

 				.add("orderAttribute", orderAttributes)

 				.add("contextState", contextState)

 				.add("isVirtual", isVirtual)

+				.add("valuePrecision", valuePrecision)

 				.add("child", child)

 				.toString();

 	}

+

+	/**

+	 * @return the labelExpression

+	 */

+	public ValueExpression getLabelExpression() {

+		return labelExpression;

+	}

+

+	/**

+	 * @param labelExpression the labelExpression to set

+	 */

+	public void setLabelExpression(ValueExpression labelExpression) {

+		this.labelExpression = labelExpression;

+	}

+

+	/**

+	 * @return the valuePrecision

+	 */

+	public ValuePrecision getValuePrecision() {

+		return valuePrecision;

+	}

+

+	/**

+	 * @param valuePrecision the valuePrecision to set

+	 */

+	public void setValuePrecision(ValuePrecision valuePrecision) {	

+		this.valuePrecision = valuePrecision != null ? valuePrecision : ValuePrecision.EXACT;

+	}

 }

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProvider.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProvider.java
index b66da44..60c3274 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProvider.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProvider.java
@@ -1,3 +1,17 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.entity;

 

 import java.util.List;

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRoot.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRoot.java
index 3d89c19..9839a29 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRoot.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRoot.java
@@ -1,9 +1,24 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.entity;

 

 import java.util.HashMap;

 import java.util.Map;

 import java.util.Optional;

 

+import org.eclipse.mdm.api.base.adapter.Attribute;

 import org.eclipse.mdm.api.base.model.Environment;

 import org.eclipse.mdm.api.dflt.ApplicationContext;

 import org.eclipse.mdm.businessobjects.utils.ServiceUtils;

@@ -143,13 +158,13 @@
 	/**

 	 * @param context

 	 * @param type

-	 * @param idAttribute

+	 * @param filterAttribute

 	 * @return NodeLevel for given type and idAttribute

 	 */

-	public NodeLevel getNodeLevel(ApplicationContext context, String type, String idAttribute) {

+	public NodeLevel getNodeLevel(ApplicationContext context, String type, String filterAttribute) {

 		NodeLevel n = getNodeLevel(context.getSourceName());

 

-		while (n != null && !nodeLevelMatches(n, type, idAttribute)) {

+		while (n != null && !nodeLevelMatches(n, type, filterAttribute)) {

 			n = n.getChild();

 		}

 

@@ -159,13 +174,29 @@
 	/**

 	 * @param nodeLevel

 	 * @param type

-	 * @param idAttribute

+	 * @param filterAttribute

 	 * @return true, if the given type and idAttribute matches the nodeLevel

 	 */

-	private boolean nodeLevelMatches(NodeLevel nodeLevel, String type, String idAttribute) {

+	/**

+	 *  TODO 05.02.2021, jst:

+	 *  Match should respect labelAttributes / value precision to work properly for nested virtual nodes of same type

+	 *  with different value precision.

+	 *  

+	 *  Current fails in scenarios like:

+	 *  

+	 *  (...)

+	 *  |

+	 *  |---Test.DateCreated - YEAR

+	 *  	|

+	 *  	|---Test.DateCreated - MONTH

+	 *  		|

+	 *  		|--- Test.DateCreated - DAY

+	 */

+	private boolean nodeLevelMatches(NodeLevel nodeLevel, String type, String filterAttribute) {

+		String first = nodeLevel.getFilterAttributes().stream().findFirst().map(Attribute::getName).orElse(null);

 		return ServiceUtils.workaroundForTypeMapping(type)

 				.equals(ServiceUtils.workaroundForTypeMapping(nodeLevel.getEntityType()))

-				&& (Strings.isNullOrEmpty(idAttribute) || idAttribute.equals(nodeLevel.getIdAttribute().getName()));

+				&& (Strings.isNullOrEmpty(filterAttribute) || filterAttribute.equals(first));

 	}

 

 }

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/Order.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/Order.java
index b85e2fa..b3c9cc7 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/Order.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/Order.java
@@ -1,3 +1,17 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.entity;

 

 /**

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/SortAttribute.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/SortAttribute.java
index 18abe11..0819a37 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/SortAttribute.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/SortAttribute.java
@@ -1,3 +1,17 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.entity;

 

 import org.eclipse.mdm.api.base.adapter.Attribute;

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/ValuePrecision.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/ValuePrecision.java
new file mode 100644
index 0000000..7daff9d
--- /dev/null
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/entity/ValuePrecision.java
@@ -0,0 +1,147 @@
+/********************************************************************************
+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+package org.eclipse.mdm.nodeprovider.entity;
+
+import java.time.LocalDateTime;
+import java.time.temporal.TemporalAdjusters;
+
+import org.eclipse.mdm.api.base.adapter.Attribute;
+import org.eclipse.mdm.api.base.model.Value;
+import org.eclipse.mdm.api.base.model.ValueType;
+import org.eclipse.mdm.api.base.query.ComparisonOperator;
+import org.eclipse.mdm.api.base.query.Condition;
+import org.eclipse.mdm.api.base.search.ContextState;
+
+public enum ValuePrecision {
+	
+	EXACT {
+		@Override public void applyOn(Value value) {
+			// Intentionally empty
+		}
+		@Override public Condition getCondition(ContextState contextState, Attribute attribute, Object value) {
+			return ComparisonOperator.EQUAL.create(contextState, attribute, value);
+		}
+	},
+	YEAR {
+		@Override public void applyOn(Value value) {
+			ValuePrecision.applyDate(value, YEAR);
+		}
+		@Override public Condition getCondition(ContextState contextState, Attribute attribute, Object value) {
+			return getDateCondition(contextState, attribute, value, YEAR);
+		}
+	},
+	MONTH {
+		@Override public void applyOn(Value value) {
+			ValuePrecision.applyDate(value, MONTH);
+		}
+		@Override public Condition getCondition(ContextState contextState, Attribute attribute, Object value) {
+			return getDateCondition(contextState, attribute, value, MONTH);
+		}
+	},
+	DAY {
+		@Override public void applyOn(Value value) {
+			ValuePrecision.applyDate(value, DAY);
+		}
+		@Override public Condition getCondition(ContextState contextState, Attribute attribute, Object value) {
+			return getDateCondition(contextState, attribute, value, DAY);
+		}
+	},
+	HOUR {
+		@Override public void applyOn(Value value) {
+			ValuePrecision.applyDate(value, HOUR);
+		}
+		@Override public Condition getCondition(ContextState contextState, Attribute attribute, Object value) {
+			return getDateCondition(contextState, attribute, value, HOUR);
+		}
+	},
+	MINUTE {
+		@Override public void applyOn(Value value) {
+			ValuePrecision.applyDate(value, MINUTE);
+		}
+		@Override public Condition getCondition(ContextState contextState, Attribute attribute, Object value) {
+			return getDateCondition(contextState, attribute, value, MINUTE);
+		}
+	};
+	
+	/**
+	 * Applies this {@link ValuePrecision} to given {@link Value}s value, if acceptable for related {@link ValueType}.
+	 * 
+	 * @param value the value
+	 */
+	abstract public void applyOn(Value value);
+	abstract public Condition getCondition(ContextState contextState, Attribute attribute, Object value);
+
+	/**
+	 * Applies this {@link ValuePrecision} to given {@link Value}s value, for {@link ValueType.DATE}.
+	 *
+	 * @param value the value
+	 */
+	private static void applyDate(Value value, ValuePrecision precision) {
+		if (value.getValueType() == ValueType.DATE) {
+			LocalDateTime dateTime = value.extract();
+			if (dateTime != null) {
+				switch (precision) {
+				case YEAR:
+					dateTime = dateTime.with(TemporalAdjusters.firstDayOfYear());
+					// no break intentionally;
+				case MONTH:
+					dateTime = dateTime.with(TemporalAdjusters.firstDayOfMonth());
+					// no break intentionally;
+				case DAY:
+					dateTime = dateTime.withHour(0);
+					// no break intentionally;
+				case HOUR:
+					dateTime = dateTime.withMinute(0);
+					// no break intentionally;
+				case MINUTE:
+					dateTime = dateTime.withSecond(0);
+					break;
+				default:
+					// Intentionally empty.
+				}
+				value.set(dateTime);
+			}
+		}
+	}
+
+	private static Condition getDateCondition(ContextState contextState, Attribute attribute, Object value, ValuePrecision precision) {
+		if (attribute.getValueType() == ValueType.DATE) {
+			LocalDateTime start = (LocalDateTime) value;
+			LocalDateTime end;
+			switch (precision) {
+			case YEAR:
+				end = start.plusYears(1);
+				break;
+			case MONTH:
+				end = start.plusMonths(1);
+				break;
+			case DAY:
+				end = start.plusDays(1);
+				break;
+			case HOUR:
+				end = start.plusHours(1);
+				break;
+			case MINUTE:
+				end = start.plusMinutes(1);
+				break;
+			default:
+				end = start.plusNanos(1);
+			}
+			return ComparisonOperator.BETWEEN.create(contextState, attribute, new LocalDateTime[] {start, end.minusNanos(1)});
+		} else {
+			return ComparisonOperator.EQUAL.create(contextState, attribute, value);
+		}
+	}
+}
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/ExpressionLanguageMethodProvider.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/ExpressionLanguageMethodProvider.java
new file mode 100644
index 0000000..6674eb6
--- /dev/null
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/ExpressionLanguageMethodProvider.java
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+package org.eclipse.mdm.nodeprovider.utils;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * All functions in the {@link ExpressionLanguageMethodProvider} are reflected in the 'fn' namespace in the expression language context.
+ * Therefore, they have to be static.
+ *
+ */
+public class ExpressionLanguageMethodProvider {
+
+	public static String formatDate(LocalDateTime input, String format) {
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
+		return input.format(formatter);
+	}
+}
diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/SerializationUtil.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/SerializationUtil.java
index b877cae..f26e463 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/SerializationUtil.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/SerializationUtil.java
@@ -1,8 +1,24 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.utils;

 

 import java.io.IOException;

 import java.io.UnsupportedEncodingException;

+import java.util.Arrays;

 import java.util.Base64;

+import java.util.List;

 import java.util.Map;

 import java.util.stream.Collectors;

 

@@ -16,6 +32,7 @@
 import org.eclipse.mdm.businessobjects.control.FilterParser;

 import org.eclipse.mdm.businessobjects.utils.ServiceUtils;

 import org.eclipse.mdm.connector.boundary.ConnectorService;

+import org.eclipse.mdm.nodeprovider.control.MDMExpressionLanguageService;

 import org.eclipse.mdm.nodeprovider.control.NodeProviderException;

 import org.eclipse.mdm.nodeprovider.entity.NodeLevel;

 import org.eclipse.mdm.nodeprovider.entity.NodeProviderRoot;

@@ -110,8 +127,6 @@
 	 * Serialize a {@link NodeLevel} and convert it to a {@link NodeLevelDTO} JSON

 	 * string.

 	 * 

-	 * @param context

-	 * @param json

 	 * @return NodeLevel

 	 */

 	public static String serializeNodeLevel(NodeLevel nl) {

@@ -226,8 +241,11 @@
 	public static NodeLevelDTO convert(NodeLevel nl) {

 		NodeLevelDTO nld = new NodeLevelDTO();

 		nld.setType(nl.getEntityType().getName());

-		nld.setIdAttribute(nl.getIdAttribute().getName());

-		nld.setLabelAttribute(nl.getLabelAttribute().getName());

+		nld.setFilterAttributes(Arrays.asList(getFilterAttribute(nl)));

+		nld.setLabelAttributes(Arrays.asList(getLabelAttribute(nl)));

+		if (nl.getLabelExpression() != null) {

+			nld.setLabelExpression(nl.getLabelExpression().getExpressionString());

+		}

 		nld.setContextState(nl.getContextState());

 		nld.setVirtual(nl.isVirtual());

 		nld.setOrderAttributes(nl.getOrderAttributes().stream()

@@ -235,6 +253,7 @@
 		if (nl.getChild() != null) {

 			nld.setChild(convert(nl.getChild()));

 		}

+		nld.setValuePrecision(nl.getValuePrecision());

 		return nld;

 	}

 

@@ -250,16 +269,34 @@
 		ModelManager mm = context.getModelManager().get();

 		EntityType e = mm.getEntityType(ServiceUtils.invertMapping(nld.getType()));

 

-		NodeLevel nl = new NodeLevel(e, e.getAttribute(nld.getIdAttribute()), e.getAttribute(nld.getLabelAttribute()));

+		List<Attribute> filterAttributes = nld.getFilterAttributes().stream()

+				.map(e::getAttribute)

+				.collect(Collectors.toList());

+

+		List<Attribute> labelAttributes = nld.getLabelAttributes().stream()

+				.map(e::getAttribute)

+				.collect(Collectors.toList());

+

+		NodeLevel nl = new NodeLevel(e, filterAttributes, labelAttributes);

 

 		nl.setContextState(nld.getContextState());

 		nl.setVirtual(nld.isVirtual());

 		nl.getOrderAttributes().addAll(nld.getOrderAttributes().entrySet().stream()

 				.map(x -> new SortAttribute(e.getAttribute(x.getKey()), x.getValue())).collect(Collectors.toList()));

-

+		MDMExpressionLanguageService inst = new MDMExpressionLanguageService();

+		nl.setLabelExpression(inst.parseValueExpression(nld.getLabelExpression()));

+		nl.setValuePrecision(nld.getValuePrecision());

 		if (nld.getChild() != null) {

 			nl.setChild(convert(context, nld.getChild()));

 		}

 		return nl;

 	}

+

+	private static String getFilterAttribute(NodeLevel nodeLevel) {

+		return nodeLevel.getFilterAttributes().stream().map(Attribute::getName).findFirst().orElseThrow(() -> new IllegalStateException("woops?!"));

+	}

+

+	private static String getLabelAttribute(NodeLevel nodeLevel) {

+		return nodeLevel.getLabelAttributes().stream().map(Attribute::getName).findFirst().orElseThrow(() -> new IllegalStateException("woops?!"));

+	}

 }

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeLevelDTO.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeLevelDTO.java
index 3843db8..4c03513 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeLevelDTO.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeLevelDTO.java
@@ -1,9 +1,25 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.utils.dto;

 

 import java.util.HashMap;

+import java.util.List;

 import java.util.Map;

 

 import org.eclipse.mdm.api.base.search.ContextState;

+import org.eclipse.mdm.nodeprovider.entity.ValuePrecision;

 import org.eclipse.mdm.nodeprovider.entity.NodeLevel;

 import org.eclipse.mdm.nodeprovider.entity.Order;

 

@@ -18,8 +34,10 @@
 public class NodeLevelDTO {

 	private String type;

 

-	private String idAttribute;

-	private String labelAttribute;

+	private List<String> filterAttributes;

+	private List<String> labelAttributes;

+	private String labelExpression;

+	private ValuePrecision valuePrecision;

 	private Map<String, Order> orderAttributes = new HashMap<>();

 

 	private ContextState contextState = null;

@@ -45,29 +63,29 @@
 	/**

 	 * @return the idAttribute

 	 */

-	public String getIdAttribute() {

-		return idAttribute;

+	public List<String> getFilterAttributes() {

+		return filterAttributes;

 	}

 

 	/**

-	 * @param idAttribute the idAttribute to set

+	 * @param filterAttributes the idAttribute to set

 	 */

-	public void setIdAttribute(String idAttribute) {

-		this.idAttribute = idAttribute;

+	public void setFilterAttributes(List<String> filterAttributes) {

+		this.filterAttributes = filterAttributes;

 	}

 

 	/**

 	 * @return the labelAttribute

 	 */

-	public String getLabelAttribute() {

-		return labelAttribute;

+	public List<String> getLabelAttributes() {

+		return labelAttributes;

 	}

 

 	/**

-	 * @param labelAttribute the labelAttribute to set

+	 * @param labelAttributes the labelAttribute to set

 	 */

-	public void setLabelAttribute(String labelAttribute) {

-		this.labelAttribute = labelAttribute;

+	public void setLabelAttributes(List<String> labelAttributes) {

+		this.labelAttributes = labelAttributes;

 	}

 

 	/**

@@ -125,4 +143,34 @@
 	public void setChild(NodeLevelDTO child) {

 		this.child = child;

 	}

+

+	/**

+	 * 

+	 * @return

+	 */

+	public String getLabelExpression() {

+		return labelExpression;

+	}

+

+	/**

+	 * 

+	 * @param labelExpression

+	 */

+	public void setLabelExpression(String labelExpression) {

+		this.labelExpression = labelExpression;

+	}

+

+	/**

+	 * @return the valuePrecision

+	 */

+	public ValuePrecision getValuePrecision() {

+		return valuePrecision;

+	}

+

+	/**

+	 * @param valuePrecision the valuePrecision to set

+	 */

+	public void setValuePrecision(ValuePrecision valuePrecision) {

+		this.valuePrecision = valuePrecision;

+	}

 }

diff --git a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeProviderRootDTO.java b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeProviderRootDTO.java
index f75f1dd..1d23d2b 100644
--- a/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeProviderRootDTO.java
+++ b/nucleus/businessobjects/src/main/java/org/eclipse/mdm/nodeprovider/utils/dto/NodeProviderRootDTO.java
@@ -1,3 +1,17 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

 package org.eclipse.mdm.nodeprovider.utils.dto;

 

 import java.util.HashMap;

diff --git a/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProviderTest.java b/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProviderTest.java
index 6c50e63..a737579 100644
--- a/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProviderTest.java
+++ b/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/control/GenericNodeProviderTest.java
@@ -1,5 +1,6 @@
 package org.eclipse.mdm.nodeprovider.control;

 

+import static java.util.stream.Collectors.toList;

 import static org.assertj.core.api.Assertions.assertThat;

 import static org.eclipse.mdm.api.odsadapter.ODSContextFactory.PARAM_NAMESERVICE;

 import static org.eclipse.mdm.api.odsadapter.ODSContextFactory.PARAM_PASSWORD;

@@ -109,8 +110,10 @@
 		ConnectorService connectorService = Mockito.mock(ConnectorService.class);

 		Mockito.when(connectorService.getContexts()).thenReturn(Arrays.asList(context));

 		Mockito.when(connectorService.getContextByName(any())).thenReturn(context);

+		

+		MDMExpressionLanguageService elService = Mockito.mock(MDMExpressionLanguageService.class);

 

-		return new GenericNodeProvider(connectorService, npr);

+		return new GenericNodeProvider(connectorService, npr, elService);

 	}

 

 	@Test

@@ -318,31 +321,31 @@
 

 		EntityType etEnv = modelManager.getEntityType(Environment.class);

 		NodeLevel env = new NodeLevel(etEnv);

-		env.getOrderAttributes().add(new SortAttribute(env.getLabelAttribute()));

+		env.getOrderAttributes().addAll(env.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		NodeLevel project = new NodeLevel(modelManager.getEntityType(Project.class));

-		project.getOrderAttributes().add(new SortAttribute(project.getLabelAttribute()));

+		project.getOrderAttributes().addAll(project.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		EntityType vehicle = modelManager.getEntityType("vehicle");

 		NodeLevel vehicleModel = new NodeLevel(vehicle, vehicle.getAttribute("model"), vehicle.getAttribute("model"));

 		vehicleModel.setVirtual(true);

 		vehicleModel.setContextState(contextState);

-		vehicleModel.getOrderAttributes().add(new SortAttribute(vehicleModel.getLabelAttribute()));

+		vehicleModel.getOrderAttributes().addAll(vehicleModel.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		NodeLevel test = new NodeLevel(modelManager.getEntityType(org.eclipse.mdm.api.base.model.Test.class));

-		test.getOrderAttributes().add(new SortAttribute(test.getLabelAttribute()));

+		test.getOrderAttributes().addAll(test.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		NodeLevel testStep = new NodeLevel(modelManager.getEntityType(TestStep.class));

-		testStep.getOrderAttributes().add(new SortAttribute(testStep.getLabelAttribute()));

+		testStep.getOrderAttributes().addAll(testStep.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		NodeLevel measurement = new NodeLevel(modelManager.getEntityType(Measurement.class));

-		measurement.getOrderAttributes().add(new SortAttribute(measurement.getLabelAttribute()));

+		measurement.getOrderAttributes().addAll(measurement.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		NodeLevel channelGroup = new NodeLevel(modelManager.getEntityType(ChannelGroup.class));

-		channelGroup.getOrderAttributes().add(new SortAttribute(channelGroup.getLabelAttribute()));

+		channelGroup.getOrderAttributes().addAll(channelGroup.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		NodeLevel channel = new NodeLevel(modelManager.getEntityType(Channel.class));

-		channel.getOrderAttributes().add(new SortAttribute(channel.getLabelAttribute()));

+		channel.getOrderAttributes().addAll(channel.getLabelAttributes().stream().map(SortAttribute::new).collect(toList()));

 

 		env.setChild(project);

 		project.setChild(vehicleModel);

diff --git a/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRootTest.java b/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRootTest.java
index b7ce35a..3aca80c 100644
--- a/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRootTest.java
+++ b/nucleus/businessobjects/src/test/java/org/eclipse/mdm/nodeprovider/entity/NodeProviderRootTest.java
@@ -6,6 +6,7 @@
 

 import org.eclipse.mdm.api.atfxadapter.ATFXContextFactory;

 import org.eclipse.mdm.api.base.ConnectionException;

+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;

@@ -57,49 +58,49 @@
 		NodeLevel nl0 = np.getNodeLevel(context, "Environment", null);

 

 		assertThat(nl0.getEntityType().getName()).isEqualTo("Environment");

-		assertThat(nl0.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl0.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl0.getContextState()).isNull();

 		assertThat(nl0.isVirtual()).isFalse();

 

 		NodeLevel nl1 = np.getNodeLevel(context, "Project", null);

 		assertThat(nl1.getEntityType().getName()).isEqualTo("Project");

-		assertThat(nl1.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl1.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl1.getContextState()).isNull();

 		assertThat(nl1.isVirtual()).isFalse();

 

 		NodeLevel nl2 = np.getNodeLevel(context, "vehicle", "model");

 		assertThat(nl2.getEntityType().getName()).isEqualTo("vehicle");

-		assertThat(nl2.getIdAttribute().getName()).isEqualTo("model");

+		assertThat(nl2.getFilterAttributes()).extracting(Attribute::getName).containsExactly("model");

 		assertThat(nl2.getContextState()).isEqualTo(ContextState.MEASURED);

 		assertThat(nl2.isVirtual()).isTrue();

 

 		NodeLevel nl3 = np.getNodeLevel(context, "Test", null);

 		assertThat(nl3.getEntityType().getName()).isEqualTo("Test");

-		assertThat(nl3.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl3.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl3.getContextState()).isNull();

 		assertThat(nl3.isVirtual()).isFalse();

 

 		NodeLevel nl4 = np.getNodeLevel(context, "TestStep", null);

 		assertThat(nl4.getEntityType().getName()).isEqualTo("TestStep");

-		assertThat(nl4.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl4.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl4.getContextState()).isNull();

 		assertThat(nl4.isVirtual()).isFalse();

 

 		NodeLevel nl5 = np.getNodeLevel(context, "Measurement", null);

 		assertThat(nl5.getEntityType().getName()).isEqualTo("MeaResult");

-		assertThat(nl5.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl5.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl5.getContextState()).isNull();

 		assertThat(nl5.isVirtual()).isFalse();

 

 		NodeLevel nl6 = np.getNodeLevel(context, "ChannelGroup", null);

 		assertThat(nl6.getEntityType().getName()).isEqualTo("SubMatrix");

-		assertThat(nl6.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl6.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl6.getContextState()).isNull();

 		assertThat(nl6.isVirtual()).isFalse();

 

 		NodeLevel nl7 = np.getNodeLevel(context, "Channel", null);

 		assertThat(nl7.getEntityType().getName()).isEqualTo("MeaQuantity");

-		assertThat(nl7.getIdAttribute().getName()).isEqualTo("Id");

+		assertThat(nl7.getFilterAttributes()).extracting(Attribute::getName).containsExactly("Id");

 		assertThat(nl7.getContextState()).isNull();

 		assertThat(nl7.isVirtual()).isFalse();

 	}

diff --git a/nucleus/businessobjects/src/test/resources/nodeprovider_example.sql b/nucleus/businessobjects/src/test/resources/nodeprovider_example.sql
index 96bcadf..8f5cf64 100644
--- a/nucleus/businessobjects/src/test/resources/nodeprovider_example.sql
+++ b/nucleus/businessobjects/src/test/resources/nodeprovider_example.sql
@@ -5,40 +5,40 @@
   "contexts" : {

     "*" : {

       "type" : "Environment",

-      "idAttribute" : "Id",

-      "labelAttribute" : "Name",

+      "filterAttributes" : ["Id"],

+      "labelAttributes" : ["Name"],

       "child" : {

         "type" : "Project",

-        "idAttribute" : "Id",

-        "labelAttribute" : "Name",

+        "filterAttributes" : ["Id"],

+        "labelAttributes" : ["Name", "Id"],

         "child" : {

           "type" : "vehicle",

-          "idAttribute" : "model",

-          "labelAttribute" : "model",

+          "filterAttributes" : ["model"],

+          "labelAttributes" : ["model", "Id"],

           "orderAttributes" : {

             "model" : "ASCENDING"

           },

           "contextState" : "MEASURED",

           "child" : {

             "type" : "Test",

-            "idAttribute" : "Id",

-            "labelAttribute" : "Name",

+            "filterAttributes" : ["Id"],

+            "labelAttributes" : ["Name"],

             "child" : {

               "type" : "TestStep",

-              "idAttribute" : "Id",

-              "labelAttribute" : "Name",

+              "filterAttributes" : ["Id"],

+              "labelAttributes" : ["Name"],

               "child" : {

                 "type" : "Measurement",

-                "idAttribute" : "Id",

-                "labelAttribute" : "Name",

+                "filterAttributes" : ["Id"],

+                "labelAttributes" : ["Name"],

                 "child" : {

                   "type" : "ChannelGroup",

-                  "idAttribute" : "Id",

-                  "labelAttribute" : "Name",

+                  "filterAttributes" : ["Id"],

+                  "labelAttributes" : ["Name"],

                   "child" : {

                     "type" : "Channel",

-                    "idAttribute" : "Id",

-                    "labelAttribute" : "Name",

+                    "filterAttributes" : ["Id"],

+                    "labelAttributes" : ["Name"],

                     "virtual" : false

                   },

                   "virtual" : false

diff --git a/nucleus/businessobjects/src/test/resources/nodeprovider_generic.json b/nucleus/businessobjects/src/test/resources/nodeprovider_generic.json
index 10f5025..7802538 100644
--- a/nucleus/businessobjects/src/test/resources/nodeprovider_generic.json
+++ b/nucleus/businessobjects/src/test/resources/nodeprovider_generic.json
@@ -4,44 +4,44 @@
 	"contexts" : {

 	    "*" : {

 		    "type" : "Environment",

-		    "idAttribute" : "Id",

-		    "labelAttribute" : "Name",

+		    "filterAttributes" : ["Id"],

+		    "labelAttributes" : ["Name"],

 		    "virtual" : false,

 		    "child" : {

 		        "type" : "Project",

-		        "idAttribute" : "Id",

-		        "labelAttribute" : "Name",

+		        "filterAttributes" : ["Id"],

+		        "labelAttributes" : ["Name", "Id"],

 		        "virtual" : false,

 		        "child" : {        

 			        "type" : "vehicle",

-			        "idAttribute" : "model",

-			        "labelAttribute" : "model",

+			        "filterAttributes" : ["model"],

+			        "labelAttributes" : ["model"],

 			        "virtual" : true,

 			        "contextState": "MEASURED",

 			        "child" : {

 			            "type" : "Test",

-			            "idAttribute" : "Id",

-			            "labelAttribute" : "Name",

+			            "filterAttributes" : ["Id"],

+			            "labelAttributes" : ["Name"],

 			            "virtual" : false,

 			            "child" : {

 			                "type" : "TestStep",

-			                "idAttribute" : "Id",

-			                "labelAttribute" : "Name",

+			                "filterAttributes" : ["Id"],

+			                "labelAttributes" : ["Name"],

 			                "virtual" : false,

 			                "child" : {

 			                    "type" : "Measurement",

-			                    "idAttribute" : "Id",

-			                    "labelAttribute" : "Name",

+			                    "filterAttributes" : ["Id"],

+			                    "labelAttributes" : ["Name"],

 			                    "virtual" : false,

 			                    "child" : {

 			                        "type" : "ChannelGroup",

-			                        "idAttribute" : "Id",

-			                        "labelAttribute" : "Name",

+			                        "filterAttributes" : ["Id"],

+			                        "labelAttributes" : ["Name"],

 			                        "virtual" : false,

 			                        "child" : {

 			                            "type" : "Channel",

-			                            "idAttribute" : "Id",

-			                            "labelAttribute" : "Name",

+			                            "filterAttributes" : ["Id"],

+			                            "labelAttributes" : ["Name"],

 			                            "virtual" : false

 			                        }

 			                    }

diff --git a/nucleus/preferences/src/main/java/org/eclipse/mdm/preferences/controller/PreferenceService.java b/nucleus/preferences/src/main/java/org/eclipse/mdm/preferences/controller/PreferenceService.java
index 2e8d4a1..942238e 100644
--- a/nucleus/preferences/src/main/java/org/eclipse/mdm/preferences/controller/PreferenceService.java
+++ b/nucleus/preferences/src/main/java/org/eclipse/mdm/preferences/controller/PreferenceService.java
@@ -124,9 +124,8 @@
 			em.flush();
 			return convert(pe);
 		} else {
-			throw new PreferenceException(
-					"Only users with role " + ADMIN_ROLE
-							+ " are allowed to save Preferences outside of the USER scope!");
+			throw new PreferenceException("Only users with role " + ADMIN_ROLE
+					+ " are allowed to save Preferences outside of the USER scope!");
 		}
 
 	}
@@ -145,9 +144,8 @@
 	}
 
 	private boolean isAllowed(Preference preference) {
-		return sessionContext.isCallerInRole(ADMIN_ROLE)
-				|| (preference.getUser() != null
-						&& preference.getUser().equalsIgnoreCase(sessionContext.getCallerPrincipal().getName()));
+		return sessionContext.isCallerInRole(ADMIN_ROLE) || (preference.getUser() != null
+				&& preference.getUser().equalsIgnoreCase(sessionContext.getCallerPrincipal().getName()));
 	}
 
 	private PreferenceMessage convert(Preference pe) {
diff --git a/nucleus/webclient/src/main/webapp/src/app/navigator/navigator.service.ts b/nucleus/webclient/src/main/webapp/src/app/navigator/navigator.service.ts
index c14fff3..84a6079 100644
--- a/nucleus/webclient/src/main/webapp/src/app/navigator/navigator.service.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/navigator/navigator.service.ts
@@ -46,6 +46,7 @@
   }
 
   fireSelectedNodeChanged(node: Node) {
+    console.log(node)
     this.selectedNodeChanged.next(node);
   }