/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.api.dataset.lib; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Nullable; /** * Describes a filter over partition keys. */ public class PartitionFilter { public static final PartitionFilter ALWAYS_MATCH = new PartitionFilter(new LinkedHashMap<String, Condition<? extends Comparable>>()); private final Map<String, Condition<? extends Comparable>> conditions; // we only allow creating a filter through the builder. private PartitionFilter(Map<String, Condition<? extends Comparable>> conditions) { this.conditions = Collections.unmodifiableMap(new LinkedHashMap<>(conditions)); } /** * This should be used for inspection or debugging only. * To match this filter, use the {@link #match} method. * * @return the individual conditions of this filter. */ public Map<String, Condition<? extends Comparable>> getConditions() { return conditions; } /** * @return the condition for a particular field. */ public Condition<? extends Comparable> getCondition(String fieldName) { return conditions.get(fieldName); } /** * Match this filter against a partition key. The key matches iff it matches all conditions. * * @throws java.lang.IllegalArgumentException if one of the field types in the partition key are incompatible */ public boolean match(PartitionKey partitionKey) { for (Map.Entry<String, Condition<? extends Comparable>> condition : getConditions().entrySet()) { Comparable value = partitionKey.getField(condition.getKey()); if (value == null || !condition.getValue().match(value)) { return false; } } return true; } @Override public String toString() { return conditions.values().toString(); } /** * Use this to create PartitionFilters. */ public static Builder builder() { return new Builder(); } /** * A builder for partition filters. */ public static class Builder { private final Map<String, Condition<? extends Comparable>> map = new LinkedHashMap<>(); /** * Add a condition for a given field name with an inclusive lower and an exclusive upper bound. * Either bound can be null, meaning unbounded in that direction. If both upper and lower bound are * null, then this condition has no effect and is not added to the filter. * * @param field The name of the partition field * @param lower the inclusive lower bound. If null, there is no lower bound. * @param upper the exclusive upper bound. If null, there is no upper bound. * @param <T> The type of the partition field * * @throws java.lang.IllegalArgumentException if the field name is null, empty, or already exists, * or if both bounds are equal (meaning the condition cannot be satisfied). */ public <T extends Comparable<T>> Builder addRangeCondition(String field, @Nullable T lower, @Nullable T upper) { if (field == null || field.isEmpty()) { throw new IllegalArgumentException("field name cannot be null or empty."); } if (map.containsKey(field)) { throw new IllegalArgumentException(String.format("Field '%s' already exists in partition filter.", field)); } if (null == lower && null == upper) { // filter is pointless if there is no bound return this; } map.put(field, new Condition<>(field, lower, upper)); return this; } /** * Add a condition that matches by equality. * * @param field The name of the partition field * @param value The value that matching field values must have * @param <T> The type of the partition field * * @throws java.lang.IllegalArgumentException if the field name is null, empty, or already exists, * or if the value is null. */ public <T extends Comparable<T>> Builder addValueCondition(String field, T value) { if (field == null || field.isEmpty()) { throw new IllegalArgumentException("field name cannot be null or empty."); } if (value == null) { throw new IllegalArgumentException("condition value cannot be null."); } if (map.containsKey(field)) { throw new IllegalArgumentException(String.format("Field '%s' already exists in partition filter.", field)); } map.put(field, new Condition<>(field, value)); return this; } /** * Create the PartitionFilter. * * @throws java.lang.IllegalStateException if no fields have been added */ public PartitionFilter build() { if (map.isEmpty()) { throw new IllegalStateException("Partition filter cannot be empty."); } return new PartitionFilter(map); } /** * @return <tt>true</tt> if no conditions have been set on this builder. */ public boolean isEmpty() { return map.isEmpty(); } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PartitionFilter that = (PartitionFilter) o; return conditions.equals(that.conditions); } @Override public int hashCode() { return conditions.hashCode(); } /** * Represents a condition on a partitioning field, by means of an inclusive lower bound and an exclusive upper bound. * As a special case, if only one value is passed to the constructor, then this represents an equality filter. * @param <T> The type of the partitioning field. */ public static class Condition<T extends Comparable<T>> { private String fieldName; private final T lower; private final T upper; private final boolean isSingleValue; Condition(String fieldName, @Nullable T lower, @Nullable T upper) { if (lower == null && upper == null) { throw new IllegalArgumentException("Either lower or upper-bound must be non-null."); } this.fieldName = fieldName; this.lower = lower; this.upper = upper; this.isSingleValue = false; } Condition(String fieldName, T value) { if (value == null) { throw new IllegalArgumentException("Value must not be null."); } this.fieldName = fieldName; this.lower = value; this.upper = null; this.isSingleValue = true; } /** * @return the field name of this condition */ public String getFieldName() { return fieldName; } /** * @return the expected value of this condition */ public T getValue() { return lower; } /** * @return the lower bound of this condition */ public T getLower() { return lower; } /** * @return the upper bound of this condition */ public T getUpper() { return upper; } /** * @return whether this condition matches a single value */ public boolean isSingleValue() { return isSingleValue; } /** * Match the condition against a given value. The value must be of the same type as the bounds of the condition. * * @throws java.lang.IllegalArgumentException if the value has an incompatible type */ public <V extends Comparable> boolean match(V value) { try { // if lower and upper are identical, then this represents an equality condition. @SuppressWarnings("unchecked") boolean matches = // the variable is redundant but required in order to suppress the warning isSingleValue() ? getValue().compareTo((T) value) == 0 : (lower == null || lower.compareTo((T) value) <= 0) && (upper == null || upper.compareTo((T) value) > 0); return matches; } catch (ClassCastException e) { // this should never happen because we make sure that partition keys and filters // match the field types declared for the partitioning. But just to be sure: throw new IllegalArgumentException("Incompatible partition filter: condition for field '" + fieldName + "' is on " + determineClass() + " but partition key value '" + value + "' is of " + value.getClass()); } } private Class<? extends Comparable> determineClass() { // either lower or upper must be non-null return lower != null ? lower.getClass() : upper.getClass(); } @Override public String toString() { if (isSingleValue()) { return fieldName + "==" + getValue().toString(); } else { return fieldName + " in [" + (getLower() == null ? "null" : getLower().toString()) + "..." + (getUpper() == null ? "null" : getUpper().toString()) + "]"; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Condition condition = (Condition) o; if (isSingleValue != condition.isSingleValue) { return false; } if (!fieldName.equals(condition.fieldName)) { return false; } if (lower != null ? !lower.equals(condition.lower) : condition.lower != null) { return false; } if (upper != null ? !upper.equals(condition.upper) : condition.upper != null) { return false; } return true; } @Override public int hashCode() { int result = fieldName.hashCode(); result = 31 * result + (lower != null ? lower.hashCode() : 0); result = 31 * result + (upper != null ? upper.hashCode() : 0); result = 31 * result + (isSingleValue ? 1 : 0); return result; } } }