blob: 7d998bea8af8f08ba79300f90f073564483ebd75 [file] [log] [blame]
/********************************************************************************
* 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.api.odsadapter.search;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.reducing;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.mdm.api.base.adapter.Attribute;
import org.eclipse.mdm.api.base.adapter.EntityType;
import org.eclipse.mdm.api.base.adapter.Relation;
import org.eclipse.mdm.api.base.model.ContextType;
import org.eclipse.mdm.api.base.model.Value;
import org.eclipse.mdm.api.base.query.Aggregation;
import org.eclipse.mdm.api.base.query.DataAccessException;
import org.eclipse.mdm.api.base.query.Filter;
import org.eclipse.mdm.api.base.query.Record;
import org.eclipse.mdm.api.base.query.Result;
import org.eclipse.mdm.api.base.search.ContextState;
import org.eclipse.mdm.api.base.search.SearchQuery;
import org.eclipse.mdm.api.base.search.Searchable;
/**
* Merges 2 distinct search queries, where one queries context data as ordered
* and the other context as measured.
*
* @since 1.0.0
* @author Viktor Stoehr, Gigatronik Ingolstadt GmbH
*/
final class MergedSearchQuery implements SearchQuery {
// ======================================================================
// Instance variables
// ======================================================================
private final EntityType entityType;
private final BaseEntitySearchQuery byResult;
private final BaseEntitySearchQuery byOrder;
// ======================================================================
// Constructors
// ======================================================================
MergedSearchQuery(EntityType entityType, Function<ContextState, BaseEntitySearchQuery> factory) {
this.entityType = entityType;
byResult = factory.apply(ContextState.MEASURED);
byOrder = factory.apply(ContextState.ORDERED);
}
// ======================================================================
// Public methods
// ======================================================================
/**
* {@inheritDoc}
*/
@Override
public List<EntityType> listEntityTypes() {
return byOrder.listEntityTypes();
}
/**
* {@inheritDoc}
*/
@Override
public Searchable getSearchableRoot() {
return byOrder.getSearchableRoot();
}
/**
* {@inheritDoc}
*/
@Override
public List<Value> getFilterValues(Attribute attribute, Filter filter) throws DataAccessException {
List<Value> orderValues = byOrder.getFilterValues(attribute, filter);
List<Value> resultValues = byResult.getFilterValues(attribute, filter);
return Stream.concat(orderValues.stream(), resultValues.stream())
// group by value and merge values
.collect(groupingBy(Value::extract, reducing((v1, v2) -> v1)))
// collect merged results
.values().stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
}
/**
* {@inheritDoc}
*/
@Override
public List<Result> getFilterResults(List<Attribute> attributes, Filter filter, ContextState contextState)
throws DataAccessException {
ContextState filterContext = filter.getContext() != null ? filter.getContext() : contextState;
List<Result> results;
Set<EntityType> entityTypesFromAttributes = getTypesFromAttributes(attributes);
if (entityTypesFromAttributes.size() > 1) {
throw new DataAccessException("Cannot handle multiple types here");
}
if (ContextState.ORDERED.equals(filterContext)) {
results = byOrder.getFilterResults(attributes, filter, contextState);
swapValues(attributes, results, Aggregation.DISTINCT);
} else {
results = byResult.getFilterResults(attributes, filter, contextState);
}
return results;
}
/**
* {@inheritDoc}
*/
@Override
public List<Result> fetchComplete(List<EntityType> entityTypes, Filter filter) throws DataAccessException {
return mergeResults(byOrder.fetchComplete(entityTypes, filter), byResult.fetchComplete(entityTypes, filter));
}
/**
* {@inheritDoc}
*/
@Override
public List<Result> fetch(List<Attribute> attributes, Filter filter) throws DataAccessException {
ContextState contextState = filter.getContext();
if (ContextState.ORDERED.equals(contextState)) {
return fetch(contextState, byOrder, byResult, attributes, filter, results -> createIdFilter(results, entityType, Aggregation.NONE, contextState));
} else if (ContextState.MEASURED.equals(contextState)) {
return fetch(contextState, byResult, byOrder, attributes, filter, results -> createIdFilter(results, entityType, Aggregation.NONE, contextState));
}
return fetch(contextState, byResult, byOrder, attributes, filter, list -> filter);
}
// ======================================================================
// Private methods
// ======================================================================
/**
* Will fetch the values based on the given primary and secondary
* {@link BaseEntitySearchQuery} with the provided {@link Filter}. Will swap
* ordered/measured values if necessary.
*
* @param contextState the {@link ContextState} were the values came from.
* @param primary the primary {@link BaseEntitySearchQuery}
* @param secondary the secondary {@link BaseEntitySearchQuery}
* @param attributes selected attributes as list of {@link Attribute}
* @param filter the current {@link Filter}
* @param filterFunction this function provides the filter for the secondary
* query
*
* @return list of merged {@link Result}
*/
private List<Result> fetch(ContextState contextState, BaseEntitySearchQuery primary,
BaseEntitySearchQuery secondary, List<Attribute> attributes, Filter filter,
Function<List<Result>, Filter> filterFunction) {
List<Result> primaryResults = primary.fetch(attributes, filter);
if (primaryResults.isEmpty()) {
// return early as secondaryResults do not matter, if no primaryResults are
// found.
return Collections.emptyList();
}
Filter additionalFilter = filterFunction.apply(primaryResults);
List<Result> secondaryResults = secondary.fetch(attributes, additionalFilter);
if (ContextState.ORDERED.equals(contextState)) {
swapValues(attributes, primaryResults, Aggregation.NONE);
} else if (ContextState.MEASURED.equals(contextState)) {
swapValues(attributes, secondaryResults, Aggregation.NONE);
}
return mergeResults(primaryResults, secondaryResults);
}
/**
* Swaps the ordered/measured values inside the {@link Value} object related to
* the given attributes.
*
* @param attributes selected attributes as list of {@link Attribute}
* @param results the list of results where the values should be swapped
*/
private void swapValues(List<Attribute> attributes, List<Result> results, Aggregation aggregation) {
Set<String> relevantNames = attributes.stream()
.filter(this::filterContextAttribute)
.map(Attribute::getName)
.map(name -> getAggregatedName(name, aggregation))
.collect(Collectors.toSet());
results.stream()
.flatMap(Result::stream)
.map(Record::getValues)
.map(Map::values)
.flatMap(Collection::stream)
.filter(value -> relevantNames.contains(value.getName()))
.forEach(Value::swapContext);
}
/**
* Check whether an attribute which belong to entities from context.
* e.g entities created as "UnitUnderTestPart", which then have a parent relation to "UnitUnderTest"
*
* @param attribute the attribute to check
* @return <code>true<</code> if the attribute belongs to a context, <code>false</code> otherwise
*/
private boolean filterContextAttribute(Attribute attribute) {
return attribute.getEntityType().getParentRelations().stream()
.map(Relation::getName)
.anyMatch(name -> ContextType.UNITUNDERTEST.typeName().equals(name) ||
ContextType.TESTEQUIPMENT.typeName().equals(name) ||
ContextType.TESTSEQUENCE.typeName().equals(name));
}
/**
* Creates a {@link Filter} based on the IDs of the given results.
*
* @param results the results where the IDs should be extracted from
*
* @return a new filter with the IDs
*/
private Filter createIdFilter(List<Result> results, EntityType currentEntityType, Aggregation aggregation, ContextState contextState) {
Filter idFilter = Filter.or();
results.stream()
.flatMap(Result::stream)
.filter(record -> hasID(record, currentEntityType, aggregation))
.collect(Collectors.groupingBy(Record::getEntityType,
Collectors.mapping(record -> getID(record, currentEntityType, aggregation, contextState), Collectors.toList())))
.forEach(idFilter::ids);
return idFilter;
}
/**
* Checks if a given {@link Record} contains a valid ID
*
* @param record the record which should be checked
*
* @return <code>true</code> if the record has a valid ID, <code>false</code>
* otherwise
*/
private boolean hasID(Record record, EntityType entityType, Aggregation aggregation) {
if (record != null) {
try {
String idKey = getAggregatedName(entityType.getIDAttribute().getName(), aggregation);
Value idValue = record.getValues().get(idKey);
return idValue != null;
} catch (IllegalStateException exception) {
return false;
}
} else {
return false;
}
}
/**
* Returns the ID of a {@ink Record} and takes {@link EntityType}, {@link Aggregation} and {@link ContextState}
* into account.
*
* @param record the record to get the ID from
* @param entityType the related entity type
* @param aggregation the aggregation which was used to create the record
* @param contextState the context state
*
* @return the ID
*/
private String getID(Record record, EntityType entityType, Aggregation aggregation, ContextState contextState) {
if (record != null) {
String idKey = getAggregatedName(entityType.getIDAttribute().getName(), aggregation);
Value idValue = record.getValues().get(idKey);
if (idValue == null) {
throw new IllegalStateException("ID attribute was not selected.");
}
return idValue.extract(contextState);
} else {
return null;
}
}
/**
* Returns the name and takes the given {@link Aggregation} into account.
*
* @param name the name to use the aggregation with
* @param aggregation the aggregation
*
* @return the updated name
*/
private String getAggregatedName(String name, Aggregation aggregation) {
String key;
if (Aggregation.NONE == aggregation) {
key = name;
} else {
key = String.format("%s(%s)", aggregation.name(), name);
}
return key;
}
/**
* Merges given {@link Result}s to one using the root entity type of this search
* query.
*
* @param results1 The first {@code Result}.
* @param results2 The second {@code Result}.
* @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(mergeType, r.getRecord(mergeType), aggregation), reducing(Result::merge)))
// collect merged results
.values().stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
}
/**
* Returns the ID of the record, if ID of ContextState.MEASURED is not set,
* returns ID of ContextState.ORDERED.
*
* @param rec {@link Record} which ID is requested.
* @return Returns the ID of the record, if ID of ContextState.MEASURED is not
* set, returns ID of ContextState.ORDERED.
*/
private String getMergeId(EntityType idType, Record rec, Aggregation aggregation) {
String idKey;
if (Aggregation.NONE == aggregation) {
idKey = idType.getIDAttribute().getName();
} else {
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;
}
}
/**
* Returns a set of {@link EntityType} used in the given attributes.
*
* @param attributes the attributes to collect the entity types from
*
* @return a set ot entity types
*/
private Set<EntityType> getTypesFromAttributes(List<Attribute> attributes) {
return attributes.stream()
.map(Attribute::getEntityType)
.collect(Collectors.toSet());
}
}