fi.nationallibrary.ndl.solr.request.RangeFieldFacets.java Source code

Java tutorial

Introduction

Here is the source code for fi.nationallibrary.ndl.solr.request.RangeFieldFacets.java

Source

/**
 * 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 fi.nationallibrary.ndl.solr.request;

import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.*;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
import org.apache.solr.common.params.FacetParams.FacetRangeInclude;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SimpleFacets;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.*;
import org.apache.solr.search.*;
import org.apache.solr.util.DateMathParser;
import org.apache.solr.handler.component.ResponseBuilder;
import fi.nationallibrary.ndl.solr.schema.RangeField;

import java.io.IOException;
import java.util.*;

/**
 * A class that generates simple Facet information for a request.
 *
 * More advanced facet implementations may compose or subclass this class 
 * to leverage any of it's functionality.
 */
public class RangeFieldFacets extends SimpleFacets {

    // per-facet values
    SolrParams localParams; // localParams on this particular facet command
    String facetValue; // the field to or query to facet on (minus local params)
    DocSet base; // the base docset for this particular facet
    String key; // what name should the results be stored under

    public RangeFieldFacets(SolrQueryRequest req, DocSet docs, SolrParams params) {
        super(req, docs, params, null);
    }

    public RangeFieldFacets(SolrQueryRequest req, DocSet docs, SolrParams params, ResponseBuilder rb) {
        super(req, docs, params, rb);
    }

    /**
     * Looks at various Params to determing if any simple Facet Constraint count
     * computations are desired.
     *
     * @see #getFacetQueryCounts
     * @see #getFacetFieldCounts
     * @see #getFacetDateCounts
     * @see #getFacetRangeCounts
     * @see FacetParams#FACET
     * @return a NamedList of Facet Count info or null
     */
    @Override
    public NamedList getFacetCounts() {

        // if someone called this method, benefit of the doubt: assume true
        if (!params.getBool(FacetParams.FACET, true))
            return null;

        facetResponse = new SimpleOrderedMap();
        try {
            facetResponse.add("facet_queries", getFacetQueryCounts());
            facetResponse.add("facet_fields", getFacetFieldCounts());
            facetResponse.add("facet_dates", getFacetDateCounts());
            facetResponse.add("facet_ranges", getFacetRangeCounts());

        } catch (IOException e) {
            SolrException.logOnce(SolrCore.log, "Exception during facet counts", e);
            throw new SolrException(ErrorCode.SERVER_ERROR, e);
        } catch (ParseException e) {
            SolrException.logOnce(SolrCore.log, "Exception during facet counts", e);
            throw new SolrException(ErrorCode.BAD_REQUEST, e);
        }
        return facetResponse;
    }

    public NamedList getFacetRangeCounts() throws IOException, ParseException {
        final NamedList resOuter = new SimpleOrderedMap();
        final String[] fields = params.getParams(FacetParams.FACET_RANGE);

        if (null == fields || 0 == fields.length)
            return resOuter;

        for (String f : fields) {
            this.getFacetRangeCounts(f, resOuter);
        }

        return resOuter;
    }

    void parseParams(String type, String param) throws ParseException, IOException {
        localParams = QueryParsing.getLocalParams(param, req.getParams());
        base = docs;
        facetValue = param;
        key = param;

        if (localParams == null)
            return;

        // remove local params unless it's a query
        if (type != FacetParams.FACET_QUERY) {
            facetValue = localParams.get(CommonParams.VALUE);
        }

        // reset set the default key now that localParams have been removed
        key = facetValue;

        // allow explicit set of the key
        key = localParams.get(CommonParams.OUTPUT_KEY, key);

        // figure out if we need a new base DocSet
        String excludeStr = localParams.get(CommonParams.EXCLUDE);
        if (excludeStr == null)
            return;

        Map tagMap = (Map) req.getContext().get("tags");
        if (tagMap != null && rb != null) {
            List<String> excludeTagList = StrUtils.splitSmart(excludeStr, ',');

            IdentityHashMap<Query, Boolean> excludeSet = new IdentityHashMap<Query, Boolean>();
            for (String excludeTag : excludeTagList) {
                Object olst = tagMap.get(excludeTag);
                // tagMap has entries of List<String,List<QParser>>, but subject to change in the future
                if (!(olst instanceof Collection))
                    continue;
                for (Object o : (Collection) olst) {
                    if (!(o instanceof QParser))
                        continue;
                    QParser qp = (QParser) o;
                    excludeSet.put(qp.getQuery(), Boolean.TRUE);
                }
            }
            if (excludeSet.size() == 0)
                return;

            List<Query> qlist = new ArrayList<Query>();

            // add the base query
            if (!excludeSet.containsKey(rb.getQuery())) {
                qlist.add(rb.getQuery());
            }

            // add the filters
            if (rb.getFilters() != null) {
                for (Query q : rb.getFilters()) {
                    if (!excludeSet.containsKey(q)) {
                        qlist.add(q);
                    }
                }
            }

            // get the new base docset for this facet
            base = searcher.getDocSet(qlist);
        }

    }

    void getFacetRangeCounts(String facetRange, NamedList resOuter) throws IOException, ParseException {

        final IndexSchema schema = searcher.getSchema();

        parseParams(FacetParams.FACET_RANGE, facetRange);
        String f = facetValue;

        SchemaField rootSf = schema.getField(f);
        FieldType rootFt = rootSf.getType();

        final SchemaField sf;
        final FieldType ft;
        if (rootFt instanceof RangeField) {
            sf = ((RangeField) rootFt).getSubField(rootSf);
            ft = ((RangeField) rootFt).getSubType();
        } else {
            sf = rootSf;
            ft = rootFt;
        }

        RangeEndpointCalculator calc = null;

        if (ft instanceof TrieField) {
            final TrieField trie = (TrieField) ft;

            switch (trie.getType()) {
            case FLOAT:
                calc = new FloatRangeEndpointCalculator(sf);
                break;
            case DOUBLE:
                calc = new DoubleRangeEndpointCalculator(sf);
                break;
            case INTEGER:
                calc = new IntegerRangeEndpointCalculator(sf);
                break;
            case LONG:
                calc = new LongRangeEndpointCalculator(sf);
                break;
            default:
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                        "Unable to range facet on tried field of unexpected type:" + f);
            }
        } else if (ft instanceof DateField) {
            calc = new DateRangeEndpointCalculator(sf, NOW);
        } else if (ft instanceof SortableIntField) {
            calc = new IntegerRangeEndpointCalculator(sf);
        } else if (ft instanceof SortableLongField) {
            calc = new LongRangeEndpointCalculator(sf);
        } else if (ft instanceof SortableFloatField) {
            calc = new FloatRangeEndpointCalculator(sf);
        } else if (ft instanceof SortableDoubleField) {
            calc = new DoubleRangeEndpointCalculator(sf);
        } else {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on field:" + sf);
        }

        resOuter.add(key, getFacetRangeCounts(rootSf, calc));
    }

    private <T extends Comparable<T>> NamedList getFacetRangeCounts(final SchemaField sf,
            final RangeEndpointCalculator<T> calc) throws IOException {

        final String f = sf.getName();
        final NamedList res = new SimpleOrderedMap();
        final NamedList counts = new NamedList();
        res.add("counts", counts);

        final T start = calc.getValue(required.getFieldParam(f, FacetParams.FACET_RANGE_START));

        // not final, hardend may change this
        T end = calc.getValue(required.getFieldParam(f, FacetParams.FACET_RANGE_END));
        if (end.compareTo(start) < 0) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                    "range facet 'end' comes before 'start': " + end + " < " + start);
        }

        final String gap = required.getFieldParam(f, FacetParams.FACET_RANGE_GAP);
        String[] gaps = parseGaps(gap);
        // explicitly return the gap.  compute this early so we are more 
        // likely to catch parse errors before attempting math
        for (int i = 0; i < gaps.length; i++) {
            calc.getGap(gaps[i]);
        }
        res.add("gap", gap);

        final int minCount = params.getFieldInt(f, FacetParams.FACET_MINCOUNT, 0);

        final EnumSet<FacetRangeInclude> include = FacetRangeInclude
                .parseParam(params.getFieldParams(f, FacetParams.FACET_RANGE_INCLUDE));

        T low = start;
        int gapIdx = 0;
        int previousCount = 0;

        while (low.compareTo(end) < 0) {
            T high = calc.addGap(low, gaps[gapIdx]);
            if (end.compareTo(high) < 0) {
                if (params.getFieldBool(f, FacetParams.FACET_RANGE_HARD_END, false)) {
                    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?)");
            }

            final boolean includeLower = (include.contains(FacetRangeInclude.LOWER)
                    || (include.contains(FacetRangeInclude.EDGE) && 0 == low.compareTo(start)));
            final boolean includeUpper = (include.contains(FacetRangeInclude.UPPER)
                    || (include.contains(FacetRangeInclude.EDGE) && 0 == high.compareTo(end)));

            final String lowS = calc.formatValue(low);
            final String highS = calc.formatValue(high);

            final int count = rangeCount(sf, lowS, highS, includeLower, includeUpper);
            if (count >= minCount && count != previousCount) {
                counts.add(lowS, count);
                previousCount = count;
            }

            low = high;

            gapIdx = Math.min(gaps.length - 1, gapIdx + 1);
        }

        // explicitly return the start and end so all the counts 
        // (including before/after/between) are meaningful - even if mincount
        // has removed the neighboring ranges
        res.add("start", start);
        res.add("end", end);

        final String[] othersP = params.getFieldParams(f, FacetParams.FACET_RANGE_OTHER);
        if (null != othersP && 0 < othersP.length) {
            Set<FacetRangeOther> others = EnumSet.noneOf(FacetRangeOther.class);

            for (final String o : othersP) {
                others.add(FacetRangeOther.get(o));
            }

            // 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);
                final String startS = calc.formatValue(start);
                final String endS = calc.formatValue(end);

                if (all || others.contains(FacetRangeOther.BEFORE)) {
                    // include upper bound if "outer" or if first gap doesn't already include it
                    res.add(FacetRangeOther.BEFORE.toString(),
                            rangeCount(sf, null, startS, false,
                                    (include.contains(FacetRangeInclude.OUTER)
                                            || (!(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
                    res.add(FacetRangeOther.AFTER.toString(),
                            rangeCount(sf, endS, null,
                                    (include.contains(FacetRangeInclude.OUTER)
                                            || (!(include.contains(FacetRangeInclude.UPPER)
                                                    || include.contains(FacetRangeInclude.EDGE)))),
                                    false));
                }
                if (all || others.contains(FacetRangeOther.BETWEEN)) {
                    res.add(FacetRangeOther.BETWEEN.toString(), rangeCount(sf, startS, endS,
                            (include.contains(FacetRangeInclude.LOWER) || include.contains(FacetRangeInclude.EDGE)),
                            (include.contains(FacetRangeInclude.UPPER)
                                    || include.contains(FacetRangeInclude.EDGE))));

                }
            }
        }
        return res;
    }

    private String[] parseGaps(String gap) {
        String gaps[] = null;
        if (gap.indexOf(",") != -1) {
            //we have variable range gaps
            gaps = gap.split(",");
        } else {
            gaps = new String[] { gap };
        }
        return gaps;
    }

    /**
     * Perhaps someday instead of having a giant "instanceof" case 
     * statement to pick an impl, we can add a "RangeFacetable" marker 
     * interface to FieldTypes and they can return instances of these 
     * directly from some method -- but until then, keep this locked down 
     * and private.
     */
    private static abstract class RangeEndpointCalculator<T extends Comparable<T>> {
        protected final SchemaField field;

        public RangeEndpointCalculator(final SchemaField field) {
            this.field = field;
        }

        /**
         * 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;

    }

    private static class FloatRangeEndpointCalculator extends RangeEndpointCalculator<Float> {

        public FloatRangeEndpointCalculator(final SchemaField f) {
            super(f);
        }

        @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());
        }
    }

    private static class DoubleRangeEndpointCalculator extends RangeEndpointCalculator<Double> {

        public DoubleRangeEndpointCalculator(final SchemaField f) {
            super(f);
        }

        @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());
        }
    }

    private static class IntegerRangeEndpointCalculator extends RangeEndpointCalculator<Integer> {

        public IntegerRangeEndpointCalculator(final SchemaField f) {
            super(f);
        }

        @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());
        }
    }

    private static class LongRangeEndpointCalculator extends RangeEndpointCalculator<Long> {

        public LongRangeEndpointCalculator(final SchemaField f) {
            super(f);
        }

        @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());
        }
    }

    private static class DateRangeEndpointCalculator extends RangeEndpointCalculator<Date> {
        private final Date now;

        public DateRangeEndpointCalculator(final SchemaField f, final Date now) {
            super(f);
            this.now = now;
            if (!(field.getType() instanceof DateField)) {
                throw new IllegalArgumentException("SchemaField must use filed type extending DateField");
            }
        }

        @Override
        public String formatValue(Date val) {
            return ((DateField) field.getType()).toExternal(val);
        }

        @Override
        protected Date parseVal(String rawval) {
            return ((DateField) 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(DateField.UTC, Locale.US);
            dmp.setNow(value);
            return dmp.parseMath(gap);
        }
    }
}