com.b2international.snowowl.snomed.core.ecl.SnomedEclRefinementEvaluator.java Source code

Java tutorial

Introduction

Here is the source code for com.b2international.snowowl.snomed.core.ecl.SnomedEclRefinementEvaluator.java

Source

/*
 * Copyright 2011-2018 B2i Healthcare Pte Ltd, http://b2i.sg
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.b2international.snowowl.snomed.core.ecl;

import static com.b2international.snowowl.datastore.index.RevisionDocument.Fields.ID;
import static com.b2international.snowowl.snomed.datastore.index.entry.SnomedRelationshipIndexEntry.Fields.DESTINATION_ID;
import static com.b2international.snowowl.snomed.datastore.index.entry.SnomedRelationshipIndexEntry.Fields.GROUP;
import static com.b2international.snowowl.snomed.datastore.index.entry.SnomedRelationshipIndexEntry.Fields.SOURCE_ID;
import static com.google.common.collect.Sets.newHashSet;
import static com.google.common.collect.Sets.newHashSetWithExpectedSize;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

import org.eclipse.xtext.util.PolymorphicDispatcher;

import com.b2international.commons.options.Options;
import com.b2international.index.query.Expression;
import com.b2international.index.query.Expressions;
import com.b2international.snowowl.core.api.SnowowlRuntimeException;
import com.b2international.snowowl.core.domain.BranchContext;
import com.b2international.snowowl.core.events.util.Promise;
import com.b2international.snowowl.core.exceptions.BadRequestException;
import com.b2international.snowowl.core.request.SearchResourceRequest;
import com.b2international.snowowl.eventbus.IEventBus;
import com.b2international.snowowl.snomed.SnomedConstants.Concepts;
import com.b2international.snowowl.snomed.common.SnomedRf2Headers;
import com.b2international.snowowl.snomed.core.domain.refset.SnomedReferenceSetMembers;
import com.b2international.snowowl.snomed.core.tree.Trees;
import com.b2international.snowowl.snomed.datastore.index.entry.SnomedRefSetMemberIndexEntry;
import com.b2international.snowowl.snomed.datastore.request.SnomedRelationshipSearchRequestBuilder;
import com.b2international.snowowl.snomed.datastore.request.SnomedRequests;
import com.b2international.snowowl.snomed.ecl.ecl.AndRefinement;
import com.b2international.snowowl.snomed.ecl.ecl.AttributeComparison;
import com.b2international.snowowl.snomed.ecl.ecl.AttributeConstraint;
import com.b2international.snowowl.snomed.ecl.ecl.AttributeGroup;
import com.b2international.snowowl.snomed.ecl.ecl.Cardinality;
import com.b2international.snowowl.snomed.ecl.ecl.Comparison;
import com.b2international.snowowl.snomed.ecl.ecl.DataTypeComparison;
import com.b2international.snowowl.snomed.ecl.ecl.DecimalValueEquals;
import com.b2international.snowowl.snomed.ecl.ecl.DecimalValueGreaterThan;
import com.b2international.snowowl.snomed.ecl.ecl.DecimalValueGreaterThanEquals;
import com.b2international.snowowl.snomed.ecl.ecl.DecimalValueLessThan;
import com.b2international.snowowl.snomed.ecl.ecl.DecimalValueLessThanEquals;
import com.b2international.snowowl.snomed.ecl.ecl.DecimalValueNotEquals;
import com.b2international.snowowl.snomed.ecl.ecl.IntegerValueEquals;
import com.b2international.snowowl.snomed.ecl.ecl.IntegerValueGreaterThan;
import com.b2international.snowowl.snomed.ecl.ecl.IntegerValueGreaterThanEquals;
import com.b2international.snowowl.snomed.ecl.ecl.IntegerValueLessThan;
import com.b2international.snowowl.snomed.ecl.ecl.IntegerValueLessThanEquals;
import com.b2international.snowowl.snomed.ecl.ecl.IntegerValueNotEquals;
import com.b2international.snowowl.snomed.ecl.ecl.NestedRefinement;
import com.b2international.snowowl.snomed.ecl.ecl.OrRefinement;
import com.b2international.snowowl.snomed.ecl.ecl.Refinement;
import com.b2international.snowowl.snomed.ecl.ecl.StringValueEquals;
import com.b2international.snowowl.snomed.ecl.ecl.StringValueNotEquals;
import com.b2international.snowowl.snomed.snomedrefset.DataType;
import com.b2international.snowowl.snomed.snomedrefset.SnomedRefSetType;
import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;

/**
 * Handles refined expression constraint evaluation.
 * @since 5.4
 * @see https://confluence.ihtsdotools.org/display/DOCECL/6.2+Refinements
 * @see https://confluence.ihtsdotools.org/display/DOCECL/6.4+Conjunction+and+Disjunction
 */
final class SnomedEclRefinementEvaluator {

    static final Set<String> INFERRED_CHARACTERISTIC_TYPES = ImmutableSet.of(Concepts.INFERRED_RELATIONSHIP,
            Concepts.ADDITIONAL_RELATIONSHIP);
    static final Set<String> STATED_CHARACTERISTIC_TYPES = ImmutableSet.of(Concepts.STATED_RELATIONSHIP,
            Concepts.ADDITIONAL_RELATIONSHIP);
    private static final int UNBOUNDED_CARDINALITY = -1;
    private static final Range<Long> ANY_GROUP = Range.closed(0L, Long.MAX_VALUE);

    private final PolymorphicDispatcher<Promise<Expression>> refinementDispatcher = PolymorphicDispatcher
            .createForSingleTarget("eval", 2, 2, this);
    private final PolymorphicDispatcher<Promise<Collection<Property>>> groupRefinementDispatcher = PolymorphicDispatcher
            .createForSingleTarget("evalGroup", 3, 3, this);

    private final EclExpression focusConcepts;

    private String expressionForm = Trees.INFERRED_FORM;

    public SnomedEclRefinementEvaluator(EclExpression focusConcepts) {
        this.focusConcepts = focusConcepts;
        this.expressionForm = focusConcepts.getExpressionForm();
    }

    public Promise<Expression> evaluate(BranchContext context, Refinement refinement) {
        return refinementDispatcher.invoke(context, refinement);
    }

    protected Promise<Expression> eval(BranchContext context, Refinement refinement) {
        return SnomedEclEvaluationRequest.throwUnsupported(refinement);
    }

    /**
     * Handles eclAttribute part of refined expression constraints.
     * @see https://confluence.ihtsdotools.org/display/DOCECL/6.3+Cardinality
     */
    protected Promise<Expression> eval(final BranchContext context, final AttributeConstraint refinement) {
        return evalRefinement(context, refinement, false, ANY_GROUP).thenWith(input -> {
            final Function<Property, Object> idProvider = refinement.isReversed() ? Property::getValue
                    : Property::getObjectId;
            final Set<String> matchingIds = FluentIterable.from(input).transform(idProvider).filter(String.class)
                    .toSet();
            // two cases here, one is the [1..x] the other is [0..x]
            final Cardinality cardinality = refinement.getCardinality();
            if (cardinality != null && cardinality.getMin() == 0 && cardinality.getMax() != UNBOUNDED_CARDINALITY) {
                // XXX internal evaluation returns negative matches, that should be excluded from the focusConcept set
                return focusConcepts.resolveToExclusionExpression(context, matchingIds);
            } else {
                return focusConcepts.resolveToAndExpression(context, matchingIds);
            }
        }).failWith(throwable -> {
            if (throwable instanceof MatchAll) {
                return focusConcepts.resolveToExpression(context);
            }
            if (throwable instanceof RuntimeException) {
                throw (RuntimeException) throwable;
            } else {
                throw new SnowowlRuntimeException(throwable);
            }
        });
    }

    /**
     * Handles conjunctions in refinement part of refined expression constraints.
     * @see https://confluence.ihtsdotools.org/display/DOCECL/6.4+Conjunction+and+Disjunction
     */
    protected Promise<Expression> eval(final BranchContext context, AndRefinement and) {
        return Promise.all(evaluate(context, and.getLeft()), evaluate(context, and.getRight())).then(input -> {
            final Expression left = (Expression) input.get(0);
            final Expression right = (Expression) input.get(1);
            return Expressions.builder().filter(left).filter(right).build();
        });
    }

    /**
     * Handles disjunctions in refinement part of refined expression constraints.
     * @see https://confluence.ihtsdotools.org/display/DOCECL/6.4+Conjunction+and+Disjunction
     */
    protected Promise<Expression> eval(final BranchContext context, OrRefinement or) {
        return Promise.all(evaluate(context, or.getLeft()), evaluate(context, or.getRight())).then(input -> {
            final Expression left = (Expression) input.get(0);
            final Expression right = (Expression) input.get(1);
            return Expressions.builder().should(left).should(right).build();
        });
    }

    /**
     * Handles nested refinements by delegating the evaluation to the nested refinement constraint.
     */
    protected Promise<Expression> eval(final BranchContext context, NestedRefinement nested) {
        return evaluate(context, nested.getNested());
    }

    /**
     * Handles evaluation of attribute refinements with groups
     * @see https://confluence.ihtsdotools.org/display/DOCECL/6.2+Refinements
     */
    protected Promise<Expression> eval(final BranchContext context, AttributeGroup group) {
        final Cardinality cardinality = group.getCardinality();
        final boolean isUnbounded = cardinality == null ? true : cardinality.getMax() == UNBOUNDED_CARDINALITY;
        final long min = cardinality == null ? 1 : cardinality.getMin();
        final long max = isUnbounded ? Long.MAX_VALUE : cardinality.getMax();
        final Range<Long> groupCardinality = Range.closed(min, max);

        if (min == 0) {
            if (isUnbounded) {
                return focusConcepts.resolveToExpression(context);
            } else {
                final Range<Long> exclusionRange = Range.closed(max + 1, Long.MAX_VALUE);
                return evaluateGroup(context, exclusionRange, group.getRefinement()).thenWith(input -> {
                    final Set<String> excludedMatches = FluentIterable.from(input).transform(Property::getObjectId)
                            .toSet();
                    return focusConcepts.resolveToExclusionExpression(context, excludedMatches);
                });
            }
        } else {
            return evaluateGroup(context, groupCardinality, group.getRefinement()).thenWith(input -> {
                final Set<String> matchingIds = FluentIterable.from(input).transform(Property::getObjectId).toSet();
                return focusConcepts.resolveToAndExpression(context, matchingIds);
            });
        }
    }

    /**
     * Evaluates refinement parts inside attribute group based refinements.
     */
    protected Promise<Collection<Property>> evaluateGroup(BranchContext context, Range<Long> groupCardinality,
            Refinement refinement) {
        return groupRefinementDispatcher.invoke(context, groupCardinality, refinement);
    }

    protected Promise<Collection<Property>> evalGroup(final BranchContext context,
            final Range<Long> groupCardinality, final Refinement refinement) {
        return SnomedEclEvaluationRequest.throwUnsupported(refinement);
    }

    /**
     * Handles attribute refinements inside attribute group refinements.
     */
    protected Promise<Collection<Property>> evalGroup(final BranchContext context,
            final Range<Long> groupCardinality, final AttributeConstraint refinement) {
        if (refinement.isReversed()) {
            throw new BadRequestException("Reversed attributes are not supported in group refinements");
        } else {
            return evalRefinement(context, refinement, true, groupCardinality).thenWith(input -> {
                final Cardinality cardinality = refinement.getCardinality();
                // two cases here, one is the [1..x] the other is [0..x]
                if (cardinality != null && cardinality.getMin() == 0
                        && cardinality.getMax() != UNBOUNDED_CARDINALITY) {
                    // XXX internal evaluation returns negative matches, that should be excluded from the focusConcept set
                    final Function<Property, Object> idProvider = refinement.isReversed() ? Property::getValue
                            : Property::getObjectId;

                    final Set<String> matchingIds = FluentIterable.from(input).transform(idProvider)
                            .filter(String.class).toSet();
                    return focusConcepts.resolveToConceptsWithGroups(context)
                            .then(new Function<Multimap<String, Integer>, Collection<Property>>() {
                                @Override
                                public Collection<Property> apply(Multimap<String, Integer> groupsById) {
                                    if (groupsById.isEmpty()) {
                                        return Sets.newHashSet();
                                    } else {
                                        final Collection<Property> matchingProperties = newHashSetWithExpectedSize(
                                                groupsById.size() - matchingIds.size());
                                        for (Entry<String, Integer> entry : groupsById.entries()) {
                                            final String id = entry.getKey();
                                            if (!matchingIds.contains(id)) {
                                                matchingProperties.add(new Property(id, entry.getValue()));
                                            }
                                        }
                                        return matchingProperties;
                                    }
                                }
                            });
                } else {
                    return Promise.immediate(input);
                }
            }).failWith(throwable -> {
                if (throwable instanceof MatchAll) {
                    return focusConcepts.resolveToConceptsWithGroups(context)
                            .then(new Function<Multimap<String, Integer>, Collection<Property>>() {
                                @Override
                                public Collection<Property> apply(Multimap<String, Integer> groupsById) {
                                    final Collection<Property> matchingProperties = newHashSetWithExpectedSize(
                                            groupsById.size());
                                    for (Entry<String, Integer> entry : groupsById.entries()) {
                                        matchingProperties.add(new Property(entry.getKey(), entry.getValue()));
                                    }
                                    return matchingProperties;
                                }
                            });
                }
                throw new SnowowlRuntimeException(throwable);
            });
        }
    }

    /**
     * Handles conjunction inside attribute group based refinements.
     */
    protected Promise<Collection<Property>> evalGroup(final BranchContext context,
            final Range<Long> groupCardinality, final AndRefinement and) {
        return Promise
                .all(evaluateGroup(context, groupCardinality, and.getLeft()),
                        evaluateGroup(context, groupCardinality, and.getRight()))
                .then(evalParts(groupCardinality, Sets::intersection));
    }

    /**
     * Handles disjunction inside attribute group based refinements.
     */
    protected Promise<Collection<Property>> evalGroup(final BranchContext context,
            final Range<Long> groupCardinality, final OrRefinement or) {
        return Promise
                .all(evaluateGroup(context, groupCardinality, or.getLeft()),
                        evaluateGroup(context, groupCardinality, or.getRight()))
                .then(evalParts(groupCardinality, Sets::union));
    }

    /**
     * Handles nested refinements inside attribute group based refinements.
     */
    protected Promise<Collection<Property>> evalGroup(final BranchContext context,
            final Range<Long> groupCardinality, final NestedRefinement nested) {
        return evaluateGroup(context, groupCardinality, nested.getNested());
    }

    /**
     * Evaluates partial results coming from a binary operator's left and right side within attribute group based refinements.
     * @param groupCardinality - the cardinality to check
     * @param groupOperator - the operator to use (AND or OR, aka {@link Sets#intersection(Set, Set)} or {@link Sets#union(Set, Set)})
     * @return a function that will can be chained via {@link Promise#then(Function)} to evaluate partial results when they are available
     */
    private Function<List<Object>, Collection<Property>> evalParts(final Range<Long> groupCardinality,
            BinaryOperator<Set<Integer>> groupOperator) {
        return input -> {
            final Collection<Property> left = (Collection<Property>) input.get(0);
            final Collection<Property> right = (Collection<Property>) input.get(1);

            final Collection<Property> matchingAttributes = newHashSet();

            // group left and right side by source ID
            final Multimap<String, Property> leftRelationshipsBySource = Multimaps.index(left,
                    Property::getObjectId);
            final Multimap<String, Property> rightRelationshipsBySource = Multimaps.index(right,
                    Property::getObjectId);

            // check that each ID has the required number of groups with left and right relationships
            for (String sourceConcept : Iterables.concat(leftRelationshipsBySource.keySet(),
                    rightRelationshipsBySource.keySet())) {
                final Multimap<Integer, Property> validGroups = ArrayListMultimap.create();

                final Collection<Property> leftSourceRelationships = leftRelationshipsBySource.get(sourceConcept);
                final Collection<Property> rightSourceRelationships = rightRelationshipsBySource.get(sourceConcept);

                final Multimap<Integer, Property> leftRelationshipsByGroup = Multimaps
                        .index(leftSourceRelationships, Property::getGroup);
                final Multimap<Integer, Property> rightRelationshipsByGroup = Multimaps
                        .index(rightSourceRelationships, Property::getGroup);

                for (Integer group : groupOperator.apply(leftRelationshipsByGroup.keySet(),
                        rightRelationshipsByGroup.keySet())) {
                    validGroups.get(group).addAll(leftRelationshipsByGroup.get(group));
                    validGroups.get(group).addAll(rightRelationshipsByGroup.get(group));
                }

                if (groupCardinality.contains((long) validGroups.keySet().size())) {
                    matchingAttributes.addAll(validGroups.values());
                }
            }
            return matchingAttributes;
        };
    }

    /**
     * Evaluates attribute refinements. 
     * @param context - the branch where the evaluation should happen
     * @param refinement - the refinement itself
     * @param grouped - whether the refinement should consider groups
     * @param groupCardinality - the cardinality to use when grouped parameter is <code>true</code>
     * @return a {@link Collection} of {@link Property} objects that match the parameters
     */
    private Promise<Collection<Property>> evalRefinement(final BranchContext context,
            final AttributeConstraint refinement, final boolean grouped, final Range<Long> groupCardinality) {
        final Cardinality cardinality = refinement.getCardinality();
        // the default cardinality is [1..*]
        final boolean isUnbounded = cardinality == null ? true : cardinality.getMax() == UNBOUNDED_CARDINALITY;
        final long min = cardinality == null ? 1 : cardinality.getMin();
        final long max = isUnbounded ? Long.MAX_VALUE : cardinality.getMax();

        final Range<Long> propertyCardinality;
        if (min == 0) {
            if (isUnbounded) {
                // zero and unbounded attributes, just match all focus concepts using the focusConcept IDs
                return Promise.fail(new MatchAll());
            } else {
                // zero bounded attributes should eval to BOOL(MUST(focus) MUST_NOT(max+1))
                propertyCardinality = Range.closed(max + 1, Long.MAX_VALUE);
            }
        } else {
            // use cardinality range specified in the expression
            propertyCardinality = Range.closed(min, max);
        }
        final Function<Property, Object> idProvider = refinement.isReversed() ? Property::getValue
                : Property::getObjectId;
        final Set<String> focusConceptIds = focusConcepts.isAnyExpression() ? Collections.emptySet()
                : grouped ? focusConcepts.resolveToConceptsWithGroups(context).getSync().keySet()
                        : focusConcepts.resolve(context).getSync();
        return evalRefinement(context, refinement, grouped, focusConceptIds)
                .then(filterByCardinality(grouped, groupCardinality, propertyCardinality, idProvider));
    }

    /**
     * Evaluates an {@link AttributeConstraint} refinement on the given focusConceptId set on the given {@link BranchContext}.
     * Grouped parameter can  
     */
    private Promise<Collection<Property>> evalRefinement(final BranchContext context,
            final AttributeConstraint refinement, final boolean grouped, final Set<String> focusConceptIds) {
        final Comparison comparison = refinement.getComparison();
        final EclSerializer serializer = context.service(EclSerializer.class);
        final Collection<String> typeConceptFilter = Collections
                .singleton(serializer.serializeWithoutTerms(refinement.getAttribute()));

        if (comparison instanceof AttributeComparison) {
            // resolve non-* focusConcept ECLs to IDs, so we can filter relationships by source/destination
            // filterByType and filterByDestination accepts ECL expressions as well, so serialize them into ECL and pass as String when required
            // if reversed refinement, then we are interested in the destinationIds otherwise we need the sourceIds
            final Collection<String> destinationConceptFilter = Collections.singleton(
                    serializer.serializeWithoutTerms(((AttributeComparison) comparison).getConstraint()));
            final Collection<String> focusConceptFilter = refinement.isReversed() ? destinationConceptFilter
                    : focusConceptIds;
            final Collection<String> valueConceptFilter = refinement.isReversed() ? focusConceptIds
                    : destinationConceptFilter;
            return evalRelationships(context, focusConceptFilter, typeConceptFilter, valueConceptFilter, grouped,
                    expressionForm);
        } else if (comparison instanceof DataTypeComparison) {
            if (grouped) {
                throw new BadRequestException(
                        "Group refinement is not supported in data type based comparison (string/numeric)");
            } else if (refinement.isReversed()) {
                throw new BadRequestException(
                        "Reversed flag is not supported in data type based comparison (string/numeric)");
            } else {
                return evalMembers(context, focusConceptIds, typeConceptFilter, (DataTypeComparison) comparison);
            }
        } else {
            return SnomedEclEvaluationRequest.throwUnsupported(comparison);
        }
    }

    private Promise<Collection<Property>> evalMembers(BranchContext context, Set<String> focusConceptIds,
            Collection<String> attributeNames, DataTypeComparison comparison) {
        final Object value;
        final DataType type;
        final SearchResourceRequest.Operator operator;
        if (comparison instanceof StringValueEquals) {
            value = ((StringValueEquals) comparison).getValue();
            type = DataType.STRING;
            operator = SearchResourceRequest.Operator.EQUALS;
        } else if (comparison instanceof StringValueNotEquals) {
            value = ((StringValueNotEquals) comparison).getValue();
            type = DataType.STRING;
            operator = SearchResourceRequest.Operator.NOT_EQUALS;
        } else if (comparison instanceof IntegerValueEquals) {
            value = ((IntegerValueEquals) comparison).getValue();
            type = DataType.INTEGER;
            operator = SearchResourceRequest.Operator.EQUALS;
        } else if (comparison instanceof IntegerValueNotEquals) {
            value = ((IntegerValueNotEquals) comparison).getValue();
            type = DataType.INTEGER;
            operator = SearchResourceRequest.Operator.NOT_EQUALS;
        } else if (comparison instanceof DecimalValueEquals) {
            value = ((DecimalValueEquals) comparison).getValue();
            type = DataType.DECIMAL;
            operator = SearchResourceRequest.Operator.EQUALS;
        } else if (comparison instanceof DecimalValueNotEquals) {
            value = ((DecimalValueNotEquals) comparison).getValue();
            type = DataType.DECIMAL;
            operator = SearchResourceRequest.Operator.NOT_EQUALS;
        } else if (comparison instanceof IntegerValueLessThan) {
            value = ((IntegerValueLessThan) comparison).getValue();
            type = DataType.INTEGER;
            operator = SearchResourceRequest.Operator.LESS_THAN;
        } else if (comparison instanceof DecimalValueLessThan) {
            value = ((DecimalValueLessThan) comparison).getValue();
            type = DataType.DECIMAL;
            operator = SearchResourceRequest.Operator.LESS_THAN;
        } else if (comparison instanceof IntegerValueLessThanEquals) {
            value = ((IntegerValueLessThanEquals) comparison).getValue();
            type = DataType.INTEGER;
            operator = SearchResourceRequest.Operator.LESS_THAN_EQUALS;
        } else if (comparison instanceof DecimalValueLessThanEquals) {
            value = ((DecimalValueLessThanEquals) comparison).getValue();
            type = DataType.DECIMAL;
            operator = SearchResourceRequest.Operator.LESS_THAN_EQUALS;
        } else if (comparison instanceof IntegerValueGreaterThan) {
            value = ((IntegerValueGreaterThan) comparison).getValue();
            type = DataType.INTEGER;
            operator = SearchResourceRequest.Operator.GREATER_THAN;
        } else if (comparison instanceof DecimalValueGreaterThan) {
            value = ((DecimalValueGreaterThan) comparison).getValue();
            type = DataType.DECIMAL;
            operator = SearchResourceRequest.Operator.GREATER_THAN;
        } else if (comparison instanceof IntegerValueGreaterThanEquals) {
            value = ((IntegerValueGreaterThanEquals) comparison).getValue();
            type = DataType.INTEGER;
            operator = SearchResourceRequest.Operator.GREATER_THAN_EQUALS;
        } else if (comparison instanceof DecimalValueGreaterThanEquals) {
            value = ((DecimalValueGreaterThanEquals) comparison).getValue();
            type = DataType.DECIMAL;
            operator = SearchResourceRequest.Operator.GREATER_THAN_EQUALS;
        } else {
            return SnomedEclEvaluationRequest.throwUnsupported(comparison);
        }
        return evalMembers(context, focusConceptIds, attributeNames, type, value, operator)
                .then(matchingMembers -> FluentIterable.from(matchingMembers)
                        .transform(input -> new Property(input.getId(), input.getReferencedComponent().getId(),
                                (String) input.getProperties().get(SnomedRf2Headers.FIELD_ATTRIBUTE_NAME),
                                input.getProperties().get(SnomedRf2Headers.FIELD_VALUE),
                                0 /*groups are not supported, all members considered ungrouped*/))
                        .toSet());
    }

    private Promise<SnomedReferenceSetMembers> evalMembers(final BranchContext context,
            final Set<String> focusConceptIds, final Collection<String> attributeNames, final DataType type,
            final Object value, SearchResourceRequest.Operator operator) {
        final Set<String> characteristicTypes = Trees.INFERRED_FORM.equals(expressionForm)
                ? INFERRED_CHARACTERISTIC_TYPES
                : STATED_CHARACTERISTIC_TYPES;
        final Options propFilter = Options.builder()
                .put(SnomedRf2Headers.FIELD_CHARACTERISTIC_TYPE_ID, characteristicTypes)
                .put(SnomedRf2Headers.FIELD_ATTRIBUTE_NAME, attributeNames)
                .put(SnomedRefSetMemberIndexEntry.Fields.DATA_TYPE, type).put(SnomedRf2Headers.FIELD_VALUE, value)
                .put(SearchResourceRequest.operator(SnomedRf2Headers.FIELD_VALUE), operator).build();
        return SnomedRequests.prepareSearchMember().all().filterByActive(true)
                .filterByReferencedComponent(focusConceptIds)
                .filterByRefSetType(Collections.singleton(SnomedRefSetType.CONCRETE_DATA_TYPE))
                .filterByProps(propFilter).build(context.id(), context.branchPath())
                .execute(context.service(IEventBus.class));
    }

    /*package*/ static Function<Collection<Property>, Collection<Property>> filterByCardinality(
            final boolean grouped, final Range<Long> groupCardinality, final Range<Long> cardinality,
            final Function<Property, Object> idProvider) {
        return matchingProperties -> {
            final Multimap<Object, Property> propertiesByMatchingIds = Multimaps.index(matchingProperties,
                    idProvider);
            final Collection<Property> properties = newHashSet();

            final Range<Long> allowedRelationshipCardinality;
            if (grouped) {
                final long minRelationships = groupCardinality.lowerEndpoint() == 0 ? cardinality.lowerEndpoint()
                        : groupCardinality.lowerEndpoint() * cardinality.lowerEndpoint();
                final long maxRelationships;
                if (groupCardinality.hasUpperBound() && cardinality.hasUpperBound()) {
                    if (groupCardinality.upperEndpoint() == Long.MAX_VALUE
                            || cardinality.upperEndpoint() == Long.MAX_VALUE) {
                        maxRelationships = Long.MAX_VALUE;
                    } else {
                        maxRelationships = groupCardinality.upperEndpoint() * cardinality.upperEndpoint();
                    }
                } else {
                    // group and relationship cardinalities are unbounded
                    maxRelationships = Long.MAX_VALUE;
                }
                allowedRelationshipCardinality = Range.closed(minRelationships, maxRelationships);
            } else {
                allowedRelationshipCardinality = cardinality;
            }

            for (Object matchingConceptId : propertiesByMatchingIds.keySet()) {
                final Collection<Property> propertiesOfConcept = propertiesByMatchingIds.get(matchingConceptId);
                if (allowedRelationshipCardinality.contains((long) propertiesOfConcept.size())) {
                    if (grouped) {
                        final Multimap<Integer, Property> indexedByGroup = FluentIterable.from(propertiesOfConcept)
                                .index(Property::getGroup);
                        // if groups should be considered as well, then check group numbers in the matching sets
                        // check that the concept has at least the right amount of groups
                        final Multimap<Integer, Property> validGroups = ArrayListMultimap.create();

                        for (Integer group : indexedByGroup.keySet()) {
                            final Collection<Property> groupedRelationships = indexedByGroup.get(group);
                            if (cardinality.contains((long) groupedRelationships.size())) {
                                validGroups.putAll(group, groupedRelationships);
                            }
                        }

                        if (groupCardinality.contains((long) validGroups.keySet().size())) {
                            properties.addAll(validGroups.values());
                        }
                    } else {
                        properties.addAll(propertiesOfConcept);
                    }
                }
            }
            return properties;
        };
    }

    /**
     * Executes a SNOMED CT Relationship search request using the given source, type, destination filters.
     * If the groupedRelationshipsOnly boolean flag is <code>true</code>, then the search will match relationships that are grouped (their groupId is greater than or equals to <code>1</code>).
     * @param context - the context where the search should happen
     * @param sourceFilter - filter for relationship sources
     * @param typeFilter - filter for relationship types
     * @param destinationFilter - filter for relationship destinations
     * @param groupedRelationshipsOnly - whether the search should consider grouped relationships only or not
     * @return a {@link Promise} of {@link Collection} of {@link Property} objects that match the criteria
     * @see SnomedRelationshipSearchRequestBuilder
     */
    /*package*/ static Promise<Collection<Property>> evalRelationships(final BranchContext context,
            final Collection<String> sourceFilter, final Collection<String> typeFilter,
            final Collection<String> destinationFilter, final boolean groupedRelationshipsOnly,
            final String expressionForm) {

        final ImmutableList.Builder<String> fieldsToLoad = ImmutableList.builder();
        fieldsToLoad.add(ID, SOURCE_ID, DESTINATION_ID);
        if (groupedRelationshipsOnly) {
            fieldsToLoad.add(GROUP);
        }

        final Set<String> characteristicTypes = Trees.INFERRED_FORM.equals(expressionForm)
                ? INFERRED_CHARACTERISTIC_TYPES
                : STATED_CHARACTERISTIC_TYPES;

        final SnomedRelationshipSearchRequestBuilder req = SnomedRequests.prepareSearchRelationship().all()
                .filterByActive(true).filterBySource(sourceFilter).filterByType(typeFilter)
                .filterByDestination(destinationFilter).filterByCharacteristicTypes(characteristicTypes)
                .setFields(fieldsToLoad.build());

        // if a grouping refinement, then filter relationships with group >= 1
        if (groupedRelationshipsOnly) {
            req.filterByGroup(1, Integer.MAX_VALUE);
        }

        return req
                .build(context.id(),
                        context.branchPath())
                .execute(context.service(IEventBus.class))
                .then(input -> input.stream().map(r -> new Property(r.getId(), r.getSourceId(), r.getTypeId(),
                        r.getDestinationId(), r.getGroup())).collect(Collectors.toSet()));
    }

    // Helper Throwable class to quickly return from attribute constraint evaluation when all matches are valid
    private static final class MatchAll extends Throwable {
    }

    /*Property data class which can represent both relationships and concrete domain members with all relevant properties required for ECL refinement evaluation*/
    static final class Property {

        private final String id;
        private final String objectId;
        private String typeId;
        private Object value;
        private Integer group;

        public Property(final String objectId, final Integer group) {
            this.id = objectId;
            this.objectId = objectId;
            this.group = group;
        }

        public Property(final String id, final String objectId, final String typeId, final Object value,
                final Integer group) {
            this.id = id;
            this.objectId = objectId;
            this.typeId = typeId;
            this.value = value;
            this.group = group;
        }

        public String getId() {
            return id;
        }

        public Integer getGroup() {
            return group;
        }

        public String getObjectId() {
            return objectId;
        }

        public String getTypeId() {
            return typeId;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Property other = (Property) obj;
            return Objects.equals(id, other.id) && Objects.equals(group, other.group);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, group);
        }

        @Override
        public String toString() {
            return "Property [id=" + id + ", objectId=" + objectId + ", typeId=" + typeId + ", value=" + value
                    + ", group=" + group + "]";
        }

    }

}