blob: c0ec919ca66239c8bb7be9c1101ef3dec5d3a0ef [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.solr.analytics.util;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import org.apache.solr.analytics.facet.RangeFacet;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams.FacetRangeInclude;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.NumericFieldType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.util.DateMathParser;
/**
* Calculates a set of {@link FacetRange}s for a given {@link RangeFacet}.
*/
public abstract class FacetRangeGenerator<T extends Comparable<T>> {
protected final SchemaField field;
protected final RangeFacet rangeFacet;
public FacetRangeGenerator(final RangeFacet rangeFacet) {
this.field = rangeFacet.getField();
this.rangeFacet = rangeFacet;
}
/**
* Formats a Range endpoint for use as a range label name in the response.
* Default Impl just uses toString()
*/
public String formatValue(final T val) {
return val.toString();
}
/**
* Parses a String param into an Range endpoint value throwing
* a useful exception if not possible
*/
public final T getValue(final String rawval) {
try {
return parseVal(rawval);
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't parse value "+rawval+" for field: " + field.getName(), e);
}
}
/**
* Parses a String param into an Range endpoint.
* Can throw a low level format exception as needed.
*/
protected abstract T parseVal(final String rawval) throws java.text.ParseException;
/**
* Parses a String param into a value that represents the gap and
* can be included in the response, throwing
* a useful exception if not possible.
*
* Note: uses Object as the return type instead of T for things like
* Date where gap is just a DateMathParser string
*/
public final Object getGap(final String gap) {
try {
return parseGap(gap);
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't parse gap "+gap+" for field: " + field.getName(), e);
}
}
/**
* Parses a String param into a value that represents the gap and
* can be included in the response.
* Can throw a low level format exception as needed.
*
* Default Impl calls parseVal
*/
protected Object parseGap(final String rawval) throws java.text.ParseException {
return parseVal(rawval);
}
/**
* Adds the String gap param to a low Range endpoint value to determine
* the corrisponding high Range endpoint value, throwing
* a useful exception if not possible.
*/
public final T addGap(T value, String gap) {
try {
return parseAndAddGap(value, gap);
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't add gap "+gap+" to value " + value + " for field: " + field.getName(), e);
}
}
/**
* Adds the String gap param to a low Range endpoint value to determine
* the corrisponding high Range endpoint value.
* Can throw a low level format exception as needed.
*/
protected abstract T parseAndAddGap(T value, String gap) throws java.text.ParseException;
public static class FacetRange {
public final String name;
public final String lower;
public final String upper;
public final boolean includeLower;
public final boolean includeUpper;
private final String facetValue;
public FacetRange(String name, String lower, String upper, boolean includeLower, boolean includeUpper) {
this.name = name;
this.lower = lower;
this.upper = upper;
this.includeLower = includeLower;
this.includeUpper = includeUpper;
String value = "(*";
if (lower != null) {
value = (includeLower ? "[" : "(") + lower;
}
value += " TO ";
if (upper == null) {
value += "*)";
} else {
value += upper + (includeUpper? "]" : ")");
}
facetValue = value;
}
@Override
public String toString() {
return facetValue;
}
}
public List<FacetRange> getRanges(){
final T start = getValue(rangeFacet.getStart());
T end = getValue(rangeFacet.getEnd()); // not final, hardend may change this
if( end.compareTo(start) < 0 ){
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "range facet 'end' comes before 'start': "+end+" < "+start);
}
// explicitly return the gap. compute this early so we are more
// likely to catch parse errors before attempting math
final List<String> gaps = rangeFacet.getGaps();
String gap = gaps.get(0);
final EnumSet<FacetRangeInclude> include = rangeFacet.getInclude();
T low = start;
List<FacetRange> ranges = new ArrayList<>();
int gapCounter = 0;
while (low.compareTo(end) < 0) {
if (gapCounter<gaps.size()) {
gap = gaps.get(gapCounter++);
}
T high = addGap(low,gap);
if (end.compareTo(high) < 0) {
if (rangeFacet.isHardEnd()){
high = end;
} else {
end = high;
}
}
if (high.compareTo(low) < 0) {
throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop (is gap negative? did the math overflow?)");
}
if (high.compareTo(low) == 0) {
throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop: gap is either zero, or too small relative start/end and caused underflow: " + low + " + " + gap + " = " + high );
}
final boolean includeLower = (include.contains(FacetRangeInclude.ALL) ||
include.contains(FacetRangeInclude.LOWER) ||
(include.contains(FacetRangeInclude.EDGE) &&
0 == low.compareTo(start)));
final boolean includeUpper = (include.contains(FacetRangeInclude.ALL) ||
include.contains(FacetRangeInclude.UPPER) ||
(include.contains(FacetRangeInclude.EDGE) &&
0 == high.compareTo(end)));
final String lowS = formatValue(low);
final String highS = formatValue(high);
ranges.add( new FacetRange(lowS,lowS,highS,includeLower,includeUpper) );
low = high;
}
final Set<FacetRangeOther> others = rangeFacet.getOthers();
if (null != others && 0 < others.size() ) {
// no matter what other values are listed, we don't do
// anything if "none" is specified.
if( !others.contains(FacetRangeOther.NONE) ) {
boolean all = others.contains(FacetRangeOther.ALL);
if (all || others.contains(FacetRangeOther.BEFORE)) {
// include upper bound if "outer" or if first gap doesn't already include it
ranges.add( new FacetRange(FacetRangeOther.BEFORE.toString(),
null, formatValue(start), false, include.contains(FacetRangeInclude.OUTER) || include.contains(FacetRangeInclude.ALL) ||
!(include.contains(FacetRangeInclude.LOWER) || include.contains(FacetRangeInclude.EDGE)) ) );
}
if (all || others.contains(FacetRangeOther.AFTER)) {
// include lower bound if "outer" or if last gap doesn't already include it
ranges.add( new FacetRange(FacetRangeOther.AFTER.toString(),
formatValue(end), null, include.contains(FacetRangeInclude.OUTER) || include.contains(FacetRangeInclude.ALL) ||
!(include.contains(FacetRangeInclude.UPPER) || include.contains(FacetRangeInclude.EDGE)), false) );
}
if (all || others.contains(FacetRangeOther.BETWEEN)) {
ranges.add( new FacetRange(FacetRangeOther.BETWEEN.toString(), formatValue(start), formatValue(end),
include.contains(FacetRangeInclude.LOWER) || include.contains(FacetRangeInclude.EDGE) || include.contains(FacetRangeInclude.ALL),
include.contains(FacetRangeInclude.UPPER) || include.contains(FacetRangeInclude.EDGE) || include.contains(FacetRangeInclude.ALL)) );
}
}
}
return ranges;
}
public static FacetRangeGenerator<? extends Comparable<?>> create(RangeFacet rangeFacet){
final SchemaField sf = rangeFacet.getField();
final FieldType ft = sf.getType();
final FacetRangeGenerator<?> calc;
if (ft instanceof NumericFieldType) {
switch (ft.getNumberType()) {
case FLOAT:
calc = new FloatFacetRangeGenerator(rangeFacet);
break;
case DOUBLE:
calc = new DoubleFacetRangeGenerator(rangeFacet);
break;
case INTEGER:
calc = new IntegerFacetRangeGenerator(rangeFacet);
break;
case LONG:
calc = new LongFacetRangeGenerator(rangeFacet);
break;
case DATE:
calc = new DateFacetRangeGenerator(rangeFacet, null);
break;
default:
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on numeric field of unexpected type: " + sf.getName());
}
} else {
throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on non-numeric field: " + sf);
}
return calc;
}
}
class IntegerFacetRangeGenerator extends FacetRangeGenerator<Integer> {
public IntegerFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }
@Override
protected Integer parseVal(String rawval) {
return Integer.valueOf(rawval);
}
@Override
public Integer parseAndAddGap(Integer value, String gap) {
return value.intValue() + Integer.valueOf(gap).intValue();
}
}
class LongFacetRangeGenerator extends FacetRangeGenerator<Long> {
public LongFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }
@Override
protected Long parseVal(String rawval) {
return Long.valueOf(rawval);
}
@Override
public Long parseAndAddGap(Long value, String gap) {
return value.longValue() + Long.valueOf(gap).longValue();
}
}
class FloatFacetRangeGenerator extends FacetRangeGenerator<Float> {
public FloatFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }
@Override
protected Float parseVal(String rawval) {
return Float.valueOf(rawval);
}
@Override
public Float parseAndAddGap(Float value, String gap) {
return value.floatValue() + Float.valueOf(gap).floatValue();
}
}
class DoubleFacetRangeGenerator extends FacetRangeGenerator<Double> {
public DoubleFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }
@Override
protected Double parseVal(String rawval) {
return Double.valueOf(rawval);
}
@Override
public Double parseAndAddGap(Double value, String gap) {
return value.doubleValue() + Double.valueOf(gap).doubleValue();
}
}
class DateFacetRangeGenerator extends FacetRangeGenerator<Date> {
private final Date now;
public DateFacetRangeGenerator(final RangeFacet rangeFacet, final Date now) {
super(rangeFacet);
this.now = now;
}
@Override
public String formatValue(Date val) {
return val.toInstant().toString();
}
@Override
protected Date parseVal(String rawval) {
return DateMathParser.parseMath(now, rawval);
}
@Override
protected Object parseGap(final String rawval) {
return rawval;
}
@Override
public Date parseAndAddGap(Date value, String gap) throws java.text.ParseException {
final DateMathParser dmp = new DateMathParser();
dmp.setNow(value);
return dmp.parseMath(gap);
}
}