| /* |
| * 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.request.RangeFacetRequest; |
| 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.SchemaField; |
| import org.apache.solr.schema.TrieDateField; |
| import org.apache.solr.schema.TrieField; |
| import org.apache.solr.util.DateMathParser; |
| |
| |
| public abstract class RangeEndpointCalculator<T extends Comparable<T>> { |
| protected final SchemaField field; |
| protected final RangeFacetRequest request; |
| |
| public RangeEndpointCalculator(final RangeFacetRequest request) { |
| this.field = request.getField(); |
| this.request = request; |
| } |
| |
| /** |
| * 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; |
| |
| 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; |
| } |
| } |
| |
| public List<FacetRange> getRanges(){ |
| |
| final T start = getValue(request.getStart()); |
| T end = getValue(request.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 String[] gaps = request.getGaps(); |
| String gap = gaps[0]; |
| |
| final EnumSet<FacetRangeInclude> include = request.getInclude(); |
| |
| T low = start; |
| |
| List<FacetRange> ranges = new ArrayList<>(); |
| |
| int gapCounter = 0; |
| |
| while (low.compareTo(end) < 0) { |
| if (gapCounter<gaps.length) { |
| gap = gaps[gapCounter++]; |
| } |
| T high = addGap(low,gap); |
| if (end.compareTo(high) < 0) { |
| if (request.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 = request.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 RangeEndpointCalculator<? extends Comparable<?>> create(RangeFacetRequest request){ |
| final SchemaField sf = request.getField(); |
| final FieldType ft = sf.getType(); |
| final RangeEndpointCalculator<?> calc; |
| if (ft instanceof TrieField) { |
| final TrieField trie = (TrieField)ft; |
| switch (trie.getType()) { |
| case FLOAT: |
| calc = new FloatRangeEndpointCalculator(request); |
| break; |
| case DOUBLE: |
| calc = new DoubleRangeEndpointCalculator(request); |
| break; |
| case INTEGER: |
| calc = new IntegerRangeEndpointCalculator(request); |
| break; |
| case LONG: |
| calc = new LongRangeEndpointCalculator(request); |
| break; |
| default: |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on tried field of unexpected type:" + sf.getName()); |
| } |
| } else if (ft instanceof TrieDateField) { |
| calc = new DateRangeEndpointCalculator(request, null); |
| } else { |
| throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on field:" + sf); |
| } |
| return calc; |
| } |
| |
| public static class FloatRangeEndpointCalculator extends RangeEndpointCalculator<Float> { |
| |
| public FloatRangeEndpointCalculator(final RangeFacetRequest request) { super(request); } |
| |
| @Override |
| protected Float parseVal(String rawval) { |
| return Float.valueOf(rawval); |
| } |
| |
| @Override |
| public Float parseAndAddGap(Float value, String gap) { |
| return new Float(value.floatValue() + Float.valueOf(gap).floatValue()); |
| } |
| |
| } |
| |
| public static class DoubleRangeEndpointCalculator extends RangeEndpointCalculator<Double> { |
| |
| public DoubleRangeEndpointCalculator(final RangeFacetRequest request) { super(request); } |
| |
| @Override |
| protected Double parseVal(String rawval) { |
| return Double.valueOf(rawval); |
| } |
| |
| @Override |
| public Double parseAndAddGap(Double value, String gap) { |
| return new Double(value.doubleValue() + Double.valueOf(gap).doubleValue()); |
| } |
| |
| } |
| |
| public static class IntegerRangeEndpointCalculator extends RangeEndpointCalculator<Integer> { |
| |
| public IntegerRangeEndpointCalculator(final RangeFacetRequest request) { super(request); } |
| |
| @Override |
| protected Integer parseVal(String rawval) { |
| return Integer.valueOf(rawval); |
| } |
| |
| @Override |
| public Integer parseAndAddGap(Integer value, String gap) { |
| return new Integer(value.intValue() + Integer.valueOf(gap).intValue()); |
| } |
| |
| } |
| |
| public static class LongRangeEndpointCalculator extends RangeEndpointCalculator<Long> { |
| |
| public LongRangeEndpointCalculator(final RangeFacetRequest request) { super(request); } |
| |
| @Override |
| protected Long parseVal(String rawval) { |
| return Long.valueOf(rawval); |
| } |
| |
| @Override |
| public Long parseAndAddGap(Long value, String gap) { |
| return new Long(value.longValue() + Long.valueOf(gap).longValue()); |
| } |
| |
| } |
| |
| public static class DateRangeEndpointCalculator extends RangeEndpointCalculator<Date> { |
| private final Date now; |
| public DateRangeEndpointCalculator(final RangeFacetRequest request, final Date now) { |
| super(request); |
| this.now = now; |
| if (! (field.getType() instanceof TrieDateField) ) { |
| throw new IllegalArgumentException("SchemaField must use field type extending TrieDateField"); |
| } |
| } |
| |
| @Override |
| public String formatValue(Date val) { |
| return ((TrieDateField)field.getType()).toExternal(val); |
| } |
| |
| @Override |
| protected Date parseVal(String rawval) { |
| return ((TrieDateField)field.getType()).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); |
| } |
| |
| } |
| } |