blob: 010bc86591aa3d21490805ed7743031722690326 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2019 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.base.query;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.mdm.api.base.adapter.EntityType;
import org.eclipse.mdm.api.base.adapter.Relation;
import org.eclipse.mdm.api.base.search.SearchQuery;
import org.eclipse.mdm.api.base.search.SearchService;
/**
* A filter is a sequence of {@link FilterItem}s containing
* {@link BooleanOperator}s or {@link Condition}s.
*
* @since 1.0.0
* @author Viktor Stoehr, Gigatronik Ingolstadt GmbH
* @author Sebastian Dirsch, Gigatronik Ingolstadt GmbH
* @see Query
* @see SearchQuery
* @see SearchService
*/
public final class Filter implements Iterable<FilterItem> {
// ======================================================================
// Instance variables
// ======================================================================
private final Deque<FilterItem> filterItems = new ArrayDeque<>();
private final FilterItem operatorItem;
// ======================================================================
// Constructors
// ======================================================================
/**
* Constructor.
*
* @param operatorItem Will be added between {@link Condition}s or merged
* filters.
*/
private Filter(FilterItem operatorItem) {
this.operatorItem = operatorItem;
}
// ======================================================================
// Public methods
// ======================================================================
/**
* Creates a new filter with an instance ID condition for given
* {@link EntityType} and instance ID.
*
* @param entityType The {@code EntityType}.
* @param id The instance ID.
* @return The created filter is returned.
* @see #id(EntityType, String)
*/
public static Filter idOnly(EntityType entityType, String id) {
return and().id(entityType, id);
}
/**
* Creates a new filter with an foreign ID condition for given {@link Relation}
* and instance ID.
*
* @param relation The {@code Relation}.
* @param id The instance ID.
* @return The created filter is returned.
* @see #id(Relation, String)
*/
public static Filter idOnly(Relation relation, String id) {
return and().id(relation, id);
}
/**
* Creates a new filter with an instance IDs condition for given
* {@link EntityType} and instance IDs.
*
* @param entityType The {@code EntityType}.
* @param ids The instance IDs.
* @return The created filter is returned.
* @see #ids(EntityType, Collection)
*/
public static Filter idsOnly(EntityType entityType, Collection<String> ids) {
return and().ids(entityType, ids);
}
/**
* Creates a new filter with an foreign IDs condition for given {@link Relation}
* and instance IDs.
*
* @param relation The {@code Relation}.
* @param ids The instance IDs.
* @return The created filter is returned.
* @see #ids(Relation, Collection)
*/
public static Filter idsOnly(Relation relation, Collection<String> ids) {
return and().ids(relation, ids);
}
/**
* Creates a new filter with an instance name condition for given
* {@link EntityType} and instance name pattern.
*
* @param entityType The {@code EntityType}.
* @param pattern Is always case sensitive and may contain wildcard
* characters as follows: "?" for one matching character and
* "*" for a sequence of matching characters.
* @return The created filter is returned.
* @see #name(EntityType, String)
*/
public static Filter nameOnly(EntityType entityType, String pattern) {
return and().name(entityType, pattern);
}
/**
* Creates a new instance of this class that implicitly adds
* {@link BooleanOperator#AND} {@code FilterItem}s between {@link Condition}s or
* merged filters.
*
* <pre>
* Filter filter = Filter.and();
* filter.add(conditionA); // conditionA
* filter.add(conditionB); // conditionA AND conditionB
* filter.invert(); // !(conditionA AND conditionB)
* filter.merge(otherFilter); // !(conditionA AND conditionB) AND
* // (otherFilter)
* </pre>
*
* @return A newly created filter is returned.
*/
public static Filter and() {
return new Filter(FilterItem.AND);
}
/**
* Creates a new instance of this class that implicitly adds
* {@link BooleanOperator#OR} {@code FilterItem}s between {@link Condition}s or
* merged filters.
*
* <pre>
* Filter filter = Filter.or();
* filter.add(conditionA); // conditionA
* filter.add(conditionB); // conditionA OR conditionB
* filter.invert(); // !(conditionA OR conditionB)
* filter.merge(otherFilter); // !(conditionA OR conditionB) OR otherFilter
* </pre>
*
* @return A newly created filter is returned.
*/
public static Filter or() {
return new Filter(FilterItem.OR);
}
/**
* Adds all {@link Condition}s to this filter.
*
* @param conditions The {@link Condition}s that will be added.
* @return Returns this filter.
* @see Filter#add(Condition)
*/
public Filter addAll(List<Condition> conditions) {
conditions.forEach(this::add);
return this;
}
/**
* Adds all {@link Condition}s to this filter.
*
* @param conditions The {@link Condition}s that will be added.
* @return Returns this filter.
* @see Filter#add(Condition)
*/
public Filter addAll(Condition... conditions) {
Arrays.stream(conditions).forEach(this::add);
return this;
}
/**
* Adds a new instance ID condition ({@link ComparisonOperator#EQUAL}) for given
* {@link EntityType} and instance ID to this filter.
*
* @param entityType The {@code EntityType}.
* @param id The instance ID.
* @return Returns this filter.
* @see #add(Condition)
*/
public Filter id(EntityType entityType, String id) {
add(ComparisonOperator.EQUAL.create(entityType.getIDAttribute(), id));
return this;
}
/**
* Adds a new foreign ID condition ({@link ComparisonOperator#EQUAL}) for given
* {@link Relation} and instance ID to this filter.
*
* @param relation The {@code Relation}.
* @param id The instance ID.
* @return Returns this filter.
* @see #add(Condition)
*/
public Filter id(Relation relation, String id) {
add(ComparisonOperator.EQUAL.create(relation.getAttribute(), id));
return this;
}
/**
* Adds a new instance IDs condition ({@link ComparisonOperator#IN_SET}) for
* given {@link EntityType} and instance IDs to this filter.
*
* @param entityType The {@code EntityType}.
* @param ids The instance IDs.
* @return Returns this filter
* @see #add(Condition)
*/
public Filter ids(EntityType entityType, Collection<String> ids) {
add(ComparisonOperator.IN_SET.create(entityType.getIDAttribute(),
ids.stream().distinct().toArray(String[]::new)));
return this;
}
/**
* Adds a new foreign IDs condition ({@link ComparisonOperator#IN_SET}) for
* given {@link Relation} and instance IDs to this filter.
*
* @param relation The {@code Relation}.
* @param ids The instance IDs.
* @return Returns this filter.
* @see #add(Condition)
*/
public Filter ids(Relation relation, Collection<String> ids) {
add(ComparisonOperator.IN_SET.create(relation.getAttribute(), ids.stream().distinct().toArray(String[]::new)));
return this;
}
/**
* Adds a instance name condition ({@link ComparisonOperator#LIKE}) for given
* {@link EntityType} and instance name pattern.
*
* <p>
* <b>NOTE:</b> If the given pattern equals "*", then no {@link Condition} will
* be added since "*" means take all.
*
* @param entityType The {@code EntityType}.
* @param pattern Is always case sensitive and may contain wildcard
* characters as follows: "?" for one matching character and
* "*" for a sequence of matching characters.
* @return Returns this filter.
* @see #add(Condition)
*/
public Filter name(EntityType entityType, String pattern) {
if (!"*".equals(pattern)) {
add(ComparisonOperator.LIKE.create(entityType.getNameAttribute(), pattern));
}
return this;
}
/**
* Adds given {@link Condition} to this filter's {@link FilterItem} sequence.
*
* @param condition {@code Condition} that will be added.
* @return Returns this filter.
*/
public Filter add(Condition condition) {
insertOperator();
filterItems.addLast(new FilterItem(condition));
return this;
}
/**
* Merges all given filters with this filter.
*
* @param filters Filters that will be merged.
* @return Returns this filter.
* @see #merge(Filter)
*/
public Filter merge(List<Filter> filters) {
filters.stream().forEach(this::merge);
return this;
}
/**
* Merges all given filters with this filter.
*
* @param filters Filters that will be merged.
* @return Returns this filter.
* @see #merge(Filter)
*/
public Filter merge(Filter... filters) {
Arrays.stream(filters).forEach(this::merge);
return this;
}
/**
* The {@link FilterItem}s of the given filter are added to this filter. If
* required, then parenthesis are implicitly added as shown in the example
* below:
*
* <pre>
* Filter filter1 = Filter.and();
* Condition a = ...;
* Condition b = ...;
* filter1.addAll(a, b); // a AND b
*
* Filter filter2 = Filter.or();
* Condition c = ...;
* Condition d = ...;
* Condition e = ...;
* filter2.addAll(c, d, e); // c OR d OR e
*
* Filter filter = Filter.or();
* filter.merge(filter1, filter2); // (filter1) OR filter2
* </pre>
*
* @param filter Filter that will be merged.
* @return Returns this filter.
*/
public Filter merge(Filter filter) {
boolean addBrackets = filter.hasMultiple() && operatorItem != filter.operatorItem && !filter.isInverted();
insertOperator();
if (addBrackets) {
filterItems.addLast(FilterItem.OPEN);
}
filter.filterItems.stream().forEach(filterItems::addLast);
if (addBrackets) {
filterItems.addLast(FilterItem.CLOSE);
}
return this;
}
/**
* Inverts this filter by prepending the {@link BooleanOperator#NOT} to the
* {@link FilterItem} sequence as shown in the following examples:
*
* <pre>
* // inverting an AND filter
* Filter andFilter = Filter.and();
* Condition a = ...; // any condition
* Condition b = ...; // another condition
* andFilter.addAll(a, b); // a AND b
* andFilter.invert(); // !(a AND b)
*
* // inverting an OR filter
* Filter orFilter = Filter.or();
* Condition a = ...; // any condition
* Condition b = ...; // another condition
* orFilter.addAll(a, b); // a OR b
* orFilter.invert(); // !(a OR b)
* orFilter.invert(); // IllegalStateException -&gt; filter is already inverted!
* </pre>
*
* @return Returns this filter.
* @throws IllegalStateException Thrown either if the current set of conditions
* is empty or this filter is already inverted.
*/
public Filter invert() {
if (filterItems.isEmpty()) {
throw new IllegalStateException("Current set of conditions is empty.");
}
if (isInverted()) {
throw new IllegalStateException("Current set of conditions is already inverted.");
} else if (hasMultiple()) {
filterItems.addFirst(FilterItem.OPEN);
filterItems.addLast(FilterItem.CLOSE);
}
filterItems.addFirst(FilterItem.NOT);
return this;
}
/**
* Checks whether this filter has no conditions at all.
*
* @return Returns {@code false} if at least one {@link Condition} is contained.
*/
public boolean isEmtpty() {
return filterItems.isEmpty();
}
/**
* Returns a sequential stream with the internally stored {@link FilterItem}s as
* its source.
*
* @return A stream over the contained {@code FilterItem}s is returned.
*/
public Stream<FilterItem> stream() {
return filterItems.stream();
}
/**
* Returns an iterator over the {@link FilterItem}s contained in this filter in
* proper sequence.
*
* @return The returned iterator does not support the {@link Iterator#remove()}
* operation.
*/
@Override
public Iterator<FilterItem> iterator() {
Iterator<FilterItem> internal = filterItems.iterator();
return new Iterator<FilterItem>() {
@Override
public boolean hasNext() {
return internal.hasNext();
}
@Override
public FilterItem next() {
return internal.next();
}
};
}
// ======================================================================
// Private methods
// ======================================================================
/**
* If required the {@link #operatorItem} is added to the end (tail) of the
* {@link FilterItem} sequence.
*/
private void insertOperator() {
if (!filterItems.isEmpty()) {
filterItems.addLast(operatorItem);
}
}
/**
* Determines whether the {@link FilterItem} sequence starts with the
* {@link BooleanOperator#NOT}.
*
* @return True if this filter is inverted.
*/
private boolean isInverted() {
return !filterItems.isEmpty() && FilterItem.NOT == filterItems.getFirst();
}
/**
* Determines whether multiple {@link FilterItem}s are contained.
*
* @return True if multiple {@code FilterItem}s are contained.
*/
private boolean hasMultiple() {
return filterItems.size() > 1;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Filter)) {
return false;
}
Filter filter = (Filter) other;
// Deque does not implement equals
return Arrays.equals(this.filterItems.toArray(new FilterItem[0]), filter.filterItems.toArray(new FilterItem[0]))
&& Objects.equals(this.operatorItem, filter.operatorItem);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(filterItems, operatorItem);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return this.stream().map(i -> Objects.toString(i)).collect(Collectors.joining(""));
}
}