/**
* Copyright 2013 Cloudera 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 org.kitesdk.data.spi.partition;
import com.google.common.base.Predicate;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.DiscreteDomains;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.kitesdk.data.spi.FieldPartitioner;
import com.google.common.base.Objects;
import org.kitesdk.data.spi.predicates.Exists;
import org.kitesdk.data.spi.predicates.In;
import org.kitesdk.data.spi.predicates.Predicates;
import org.kitesdk.data.spi.predicates.Range;
import org.kitesdk.data.spi.predicates.Ranges;
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value={
"NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE",
"SE_COMPARATOR_SHOULD_BE_SERIALIZABLE"},
justification="False positive due to generics.")
@Immutable
public class RangeFieldPartitioner extends FieldPartitioner<String, String> {
private final List<String> upperBounds;
// lazily constructed DiscreteDomain for upper bounds; use domain()
private final RangeDomain domain;
public RangeFieldPartitioner(String sourceName, String... upperBounds) {
this(sourceName, null, upperBounds);
}
public RangeFieldPartitioner(String sourceName, @Nullable String name,
String... upperBounds) {
super(sourceName, (name == null ? sourceName + "_bound" : name),
String.class, String.class, upperBounds.length);
this.upperBounds = ImmutableList.copyOf(upperBounds);
this.domain = new RangeDomain();
}
@Override
public String apply(String value) {
// always return the same String object so identity comparison can be used
for (String upper : upperBounds) {
if (value.compareTo(upper) <= 0) {
return upper;
}
}
throw new IllegalArgumentException(value + " is outside bounds");
}
@Override
public Predicate<String> project(Predicate<String> predicate) {
if (predicate instanceof Exists) {
return Predicates.exists();
} else if (predicate instanceof In) {
return ((In<String>) predicate).transform(this);
} else if (predicate instanceof Range) {
// must use a closed range:
// if this( abc ) => b then this( acc ) => b, so b must be included
Range<String> transformed = Ranges.transformClosed(
(Range<String>) predicate, this);
return Predicates.in(Ranges.asSet(transformed, domain));
} else {
return null;
}
}
@Override
public Predicate<String> projectStrict(Predicate<String> predicate) {
if (predicate instanceof Exists) {
return Predicates.exists();
} else if (predicate instanceof In) {
// not possible to check all inputs to the predicate
return null;
} else if (predicate instanceof Range) {
Range<String> transformed = transformClosed((Range<String>) predicate);
if (transformed != null) {
return Predicates.in(Ranges.asSet(transformed, domain));
}
}
return null;
}
public List<String> getUpperBounds() {
return upperBounds;
}
@Override
@edu.umd.cs.findbugs.annotations.SuppressWarnings(
value="NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION",
justification="Default annotation is not correct for equals")
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || !getClass().equals(o.getClass())) {
return false;
}
RangeFieldPartitioner that = (RangeFieldPartitioner) o;
return Objects.equal(this.getName(), that.getName()) &&
Objects.equal(this.upperBounds, that.upperBounds);
}
@Override
public int compare(String o1, String o2) {
return apply(o1).compareTo(apply(o2));
}
@Override
public int hashCode() {
return Objects.hashCode(getName(), upperBounds);
}
@Override
public String toString() {
return Objects.toStringHelper(this).add("name", getName())
.add("upperBounds", upperBounds).toString();
}
/**
* Transforms a Range predicate to a closed range on this partitioner's upper
* bounds. Handles edge cases correctly.
*
* @param range a Range of Strings
* @return a Range of upper-bound Strings
*/
private Range<String> transformClosed(Range<String> range) {
if (range.hasLowerBound()) {
String lower = range.lowerEndpoint();
// the special case, (a, _] and apply(a) == a is handled by skipping a
String afterLower = domain.next(apply(lower));
if (afterLower != null) {
if (range.hasUpperBound()) {
String upper = range.upperEndpoint();
String upperImage = apply(upper);
// meaning: at the endpoint
if (upper.equals(upperImage) && range.isUpperBoundClosed()) {
// include upper
return Ranges.closed(afterLower, upperImage);
} else {
String beforeUpper = domain.previous(upperImage);
if (afterLower.compareTo(beforeUpper) <= 0) {
return Ranges.closed(afterLower, beforeUpper);
}
}
} else {
return Ranges.atLeast(afterLower);
}
}
} else if (range.hasUpperBound()) {
String upper = range.upperEndpoint();
String upperImage = apply(upper);
if (upper.equals(upperImage) && range.isUpperBoundClosed()) {
// include upper
return Ranges.atMost(upperImage);
} else {
String beforeUpper = domain.previous(upperImage);
if (beforeUpper != null) {
return Ranges.atMost(beforeUpper);
}
}
}
return null;
}
/**
* A DiscreteDomain for this partitioner's set of upper bounds.
*
* Used for Range#asSet, Predicates.transformClosed,
* Predicates.excludeEndpoints
*
* This DiscreteDomain will throw IllegalArgumentException for values that
* are after the last upper bound, and will throw IndexOutOfBoundsException
* when next or previous is called on the last or first bound
*/
private class RangeDomain extends DiscreteDomain<String> {
@Override
public String next(String value) {
// using apply ensures indexOf returns a real index
int nextIndex = upperBounds.indexOf(apply(value)) + 1;
return nextIndex == upperBounds.size() ? null : upperBounds.get(nextIndex);
}
@Override
public String previous(String value) {
// using apply ensures indexOf returns a real index
int index = upperBounds.indexOf(apply(value));
return index == 0 ? null : upperBounds.get(index - 1);
}
@Override
public long distance(String start, String end) {
// using apply ensures indexOf returns a real index
return (upperBounds.indexOf(apply(end)) -
upperBounds.indexOf(apply(start)));
}
@Override
public String minValue() {
return upperBounds.get(0);
}
@Override
public String maxValue() {
DiscreteDomains.integers();
return upperBounds.get(upperBounds.size() - 1);
}
}
}