Package org.jahia.services.search.facets

Source Code of org.jahia.services.search.facets.SimpleJahiaJcrFacets

/**
* This file is part of Jahia, next-generation open source CMS:
* Jahia's next-generation, open source CMS stems from a widely acknowledged vision
* of enterprise application convergence - web, search, document, social and portal -
* unified by the simplicity of web content management.
*
* For more information, please visit http://www.jahia.com.
*
* Copyright (C) 2002-2011 Jahia Solutions Group SA. All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* As a special exception to the terms and conditions of version 2.0 of
* the GPL (or any later version), you may redistribute this Program in connection
* with Free/Libre and Open Source Software ("FLOSS") applications as described
* in Jahia's FLOSS exception. You should have received a copy of the text
* describing the FLOSS exception, and it is also available here:
* http://www.jahia.com/license
*
* Commercial and Supported Versions of the program (dual licensing):
* alternatively, commercial and supported versions of the program may be used
* in accordance with the terms and conditions contained in a separate
* written agreement between you and Jahia Solutions Group SA.
*
* If you are unsure which license is appropriate for your use,
* please contact the sales department at sales@jahia.com.
*/

package org.jahia.services.search.facets;

import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.core.query.lucene.FieldNames;
import org.apache.jackrabbit.core.query.lucene.JahiaNodeIndexer;
import org.apache.jackrabbit.core.query.lucene.NamePathResolverImpl;
import org.apache.jackrabbit.core.query.lucene.SearchIndex;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.KeywordAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.util.DocIdBitSet;
import org.apache.lucene.util.OpenBitSet;
import org.apache.lucene.util.OpenBitSetDISI;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.params.RequiredSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.FacetParams.FacetDateOther;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.schema.DateField;
import org.apache.solr.util.DateMathParser;
import org.jahia.exceptions.JahiaException;
import org.jahia.services.content.nodetypes.ExtendedPropertyDefinition;
import org.jahia.services.content.nodetypes.NodeTypeRegistry;
import org.jahia.services.content.nodetypes.renderer.ChoiceListRenderer;
import org.jahia.services.content.nodetypes.renderer.ChoiceListRendererService;
import org.jahia.utils.LanguageCodeConverters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

/**
* 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 SimpleJahiaJcrFacets {
    /**
     * The logger instance for this class.
     */
    private static final Logger logger = LoggerFactory.getLogger(SimpleJahiaJcrFacets.class);

    public static final String PROPNAME_INDEX_SEPARATOR = "#";

    /** The main set of documents all facet counts should be relative to */
    protected OpenBitSet docs;
    /** Configuration params behavior should be driven by */
    protected SolrParams params;
    /** Searcher to use for all calculations */
    protected IndexSearcher searcher;

    protected Analyzer defaultAnalyzer;
    /**
     * Name and Path resolver.
     */
    protected final NamePathResolver resolver;
   
    /**
     * Name and Path resolver.
     */
    protected final Session session;   

    public SimpleJahiaJcrFacets(IndexSearcher searcher, OpenBitSet docs,
            SolrParams params, SearchIndex index, Session session) {
        this.searcher = searcher;
        this.docs = docs;
        this.params = params;
        this.resolver = NamePathResolverImpl.create(index.getNamespaceMappings());
        this.defaultAnalyzer = index.getTextAnalyzer();
        this.session = session;
    }

    /**
     * Looks at various Params to determing if any simple Facet Constraint count computations are desired.
     *
     * @see #getFacetQueryCounts
     * @see #getFacetFieldCounts
     * @see #getFacetDateCounts
     * @see FacetParams#FACET
     * @return a NamedList of Facet Count info or null
     */
    public NamedList<Object> getFacetCounts() {
        // if someone called this method, benefit of the doubt: assume true
        if (!params.getBool(FacetParams.FACET, true)) {
            return null;
        }

        NamedList<Object> res = new SimpleOrderedMap<Object>();
        try {

            res.add("facet_queries", getFacetQueryCounts());
            res.add("facet_fields", getFacetFieldCounts());
            res.add("facet_dates", getFacetDateCounts());

        } catch (Exception e) {
            logger.warn("Exception during facet counts", e);
            res.add("exception", e.toString());
        }
        return res;
    }

    /**
     * Returns a list of facet counts for each of the facet queries specified in the params
     *
     * @see FacetParams#FACET_QUERY
     */
    public NamedList<Object> getFacetQueryCounts() throws IOException, ParseException {

        NamedList<Object> res = new SimpleOrderedMap<Object>();

        /*
         * Ignore SolrParams.DF - could have init param facet.query assuming the schema default with query param DF intented to only affect
         * Q. If user doesn't want schema default for facet.query, they should be explicit.
         */
        // SolrQueryParser qp = searcher.getSchema().getSolrQueryParser(null);

        String[] facetQs = params.getParams(FacetParams.FACET_QUERY);
        if (null != facetQs && 0 != facetQs.length) {
            Integer mincount = params.getFieldInt("", FacetParams.FACET_MINCOUNT);
            if (mincount == null) {
                Boolean zeros = params.getFieldBool("", FacetParams.FACET_ZEROS);
                // mincount = (zeros!=null && zeros) ? 0 : 1;
                mincount = (zeros != null && !zeros) ? 1 : 0;
                // current default is to include zeros.
            }           
            for (String q : facetQs) {
                QueryParser qp = new JahiaQueryParser(FieldNames.FULLTEXT, new KeywordAnalyzer());
                qp.setLowercaseExpandedTerms(false);
                Query qobj = qp.parse(q);
                long count = OpenBitSet.intersectionCount(getDocIdSetForHits(searcher.search(qobj), ""),
                        docs);
                if (count >= mincount) {
                    res.add(q, count);
                }
            }
        }

        return res;
    }

    public NamedList<Object> getTermCounts(String field, ExtendedPropertyDefinition epd, String fieldNameInIndex, String locale)
            throws IOException, RepositoryException {
        int offset = params.getFieldInt(field, FacetParams.FACET_OFFSET, 0);
        int limit = params.getFieldInt(field, FacetParams.FACET_LIMIT, 100);
        if (limit == 0)
            return new NamedList<Object>();
        Integer mincount = params.getFieldInt(field, FacetParams.FACET_MINCOUNT);
        if (mincount == null) {
            Boolean zeros = params.getFieldBool(field, FacetParams.FACET_ZEROS);
            // mincount = (zeros!=null && zeros) ? 0 : 1;
            mincount = (zeros != null && !zeros) ? 1 : 0;
            // current default is to include zeros.
        }
        boolean missing = params.getFieldBool(field, FacetParams.FACET_MISSING, false);
        // default to sorting if there is a limit.
        boolean sort = params.getFieldBool(field, FacetParams.FACET_SORT, limit > 0);
        String prefix = params.getFieldParam(field, FacetParams.FACET_PREFIX);

        NamedList<Object> counts;

        if (epd.isMultiple() || epd.getIndex() == ExtendedPropertyDefinition.INDEXED_TOKENIZED
                || epd.getRequiredType() == PropertyType.BOOLEAN) {
            // Always use filters for booleans... we know the number of values is very small.
            counts = getFacetTermEnumCounts(searcher, docs, field, fieldNameInIndex, offset, limit, mincount,
                    missing, sort, prefix, epd.isInternationalized() ? (locale == null ? "" : locale): null);
        } else {
            // TODO: future logic could use filters instead of the fieldcache if
            // the number of terms in the field is small enough.
            counts = getFieldCacheCounts(searcher, docs, fieldNameInIndex, offset, limit, mincount, missing, sort,
                    prefix, epd.isInternationalized() ? (locale == null ? "" : locale): null);
        }

        return counts;
    }

    /**
     * Returns a list of value constraints and the associated facet counts for each facet field specified in the params.
     *
     * @see FacetParams#FACET_FIELD
     * @see #getFieldMissingCount
     * @see #getFacetTermEnumCounts
     */
    public NamedList<Object> getFacetFieldCounts() throws IOException {

        NamedList<Object> res = new SimpleOrderedMap<Object>();
        String[] facetFs = params.getParams(FacetParams.FACET_FIELD);
        if (null != facetFs) {
            for (String f : facetFs) {
                try {
                    String fieldName = StringUtils.substringBeforeLast(f, PROPNAME_INDEX_SEPARATOR);
                    String locale = params.getFieldParam(f, "facet.locale");
                    ExtendedPropertyDefinition epd = NodeTypeRegistry.getInstance().getNodeType(
                            params.get("f." + f + ".facet.nodetype")).getPropertyDefinition(
                            fieldName);
                    String fieldNameInIndex = getFieldNameInIndex(fieldName, epd, locale);
                    res.add(StringUtils.substringBeforeLast(f, PROPNAME_INDEX_SEPARATOR)
                            + PROPNAME_INDEX_SEPARATOR + fieldNameInIndex,
                            ensureSorting(params.getFieldParam(f, "facet.sort"), getTermCounts(f, epd, fieldNameInIndex, locale), epd, params.getFieldParam(f, "facet.labelRenderer"), LanguageCodeConverters.getLocaleFromCode(locale)));
                } catch (RepositoryException e) {
                    logger.error("Cant display facets for: " + f, e);
                }
            }
        }
        return res;
    }
   
    private NamedList<Object> ensureSorting(String fieldSort,
            NamedList<Object> values, ExtendedPropertyDefinition fieldPropertyType,
            String facetValueRenderer, Locale locale) {
        ChoiceListRenderer renderer = !StringUtils.isEmpty(facetValueRenderer) ? ChoiceListRendererService
                .getInstance().getRenderers().get(facetValueRenderer)
                : null;
        if (values.size() > 1
                && (fieldSort != null && (fieldSort.equals("false") || fieldSort.equals("index")))
                && renderer != null) {
            try {
                SortedMap<String, Integer> sortedLabels = new TreeMap<String, Integer>();
                int i = 0;

                for (Map.Entry<String, Object> facetValue : values) {
                    sortedLabels.put(renderer.getStringRendering(locale,
                            fieldPropertyType, facetValue.getKey()), i++);
                }
                NamedList<Object> sortedValues = new NamedList<Object>();
                for (Integer index : sortedLabels.values()) {
                    sortedValues.add(values.getName(index), values.getVal(index));
                }
                values = sortedValues;
            } catch (RepositoryException e) {
                logger.warn("Repository exception while sorting label rendered facet values, fallback to default sorting", e);
            } catch (UnsupportedOperationException e) {
                logger.warn("Unsupported operation exception while sorting label rendered facet values, fallback to default sorting", e);               
            }
        }
        return values;
    }

    /**
     * Returns a count of the documents in the set which do not have any terms for for the specified field.
     *
     * @see FacetParams#FACET_MISSING
     */
    public int getFieldMissingCount(IndexSearcher searcher, OpenBitSet docs, String fieldName, String locale)
            throws IOException {
        Query query = null;
        if (StringUtils.isEmpty(locale)) {
            query = new ConstantScoreRangeQuery(fieldName, null, null, false, false);
        } else {
            BooleanQuery booleanQuery = new BooleanQuery();
            booleanQuery.add(new ConstantScoreRangeQuery(fieldName, null, null, false, false),
                    BooleanClause.Occur.MUST);
            booleanQuery.add(
                    new TermQuery(new Term(JahiaNodeIndexer.TRANSLATION_LANGUAGE, locale)),
                    BooleanClause.Occur.MUST);
            query = booleanQuery;
        }
        OpenBitSet hasVal = getDocIdSetForHits(searcher.search(query), locale);
        return (int) OpenBitSet.andNotCount(docs, hasVal);
    }

    // first element of the fieldcache is null, so we need this comparator.
    private static final Comparator<String> nullStrComparator = new Comparator<String>() {
        public int compare(String o1, String o2) {
            if (o1 == null)
                return (o2 == null) ? 0 : -1;
            else if (o2 == null)
                return 1;
            return (o1.compareTo(o2));
        }
    };

    /**
     * Use the Lucene FieldCache to get counts for each unique field value in <code>docs</code>. The field must have at most one indexed
     * token per document.
     */
    public NamedList<Object> getFieldCacheCounts(IndexSearcher searcher, OpenBitSet docs,
            String fieldName, int offset, int limit, int mincount, boolean missing, boolean sort,
            String prefix, String locale) throws IOException {
        // TODO: If the number of terms is high compared to docs.size(), and zeros==false,
        // we should use an alternate strategy to avoid
        // 1) creating another huge int[] for the counts
        // 2) looping over that huge int[] looking for the rare non-zeros.
        //
        // Yet another variation: if docs.size() is small and termvectors are stored,
        // then use them instead of the FieldCache.
        //

        // TODO: this function is too big and could use some refactoring, but
        // we also need a facet cache, and refactoring of SimpleFacets instead of
        // trying to pass all the various params around.

        NamedList<Object> res = new NamedList<Object>();

        FieldCache.StringIndex si = FieldCache.DEFAULT.getStringIndex(searcher.getIndexReader(),
                fieldName);
        final String[] terms = si.lookup;
        final int[] termNum = si.order;

        if (prefix != null && prefix.length() == 0)
            prefix = null;

        int startTermIndex, endTermIndex;
        if (prefix != null) {
            startTermIndex = Arrays.binarySearch(terms, prefix, nullStrComparator);
            if (startTermIndex < 0)
                startTermIndex = -startTermIndex - 1;
            // find the end term. \uffff isn't a legal unicode char, but only compareTo
            // is used, so it should be fine, and is guaranteed to be bigger than legal chars.
            endTermIndex = Arrays.binarySearch(terms, prefix + "\uffff\uffff\uffff\uffff",
                    nullStrComparator);
            endTermIndex = -endTermIndex - 1;
        } else {
            startTermIndex = 1;
            endTermIndex = terms.length;
        }

        final int nTerms = endTermIndex - startTermIndex;

        if (nTerms > 0 && docs.size() >= mincount) {

            // count collection array only needs to be as big as the number of terms we are
            // going to collect counts for.
            final int[] counts = new int[nTerms];

            DocIdSetIterator iter = docs.iterator();
            while (iter.next()) {
                int term = termNum[iter.doc()];
                int arrIdx = term - startTermIndex;
                if (arrIdx >= 0 && arrIdx < nTerms)
                    counts[arrIdx]++;
            }

            // IDEA: we could also maintain a count of "other"... everything that fell outside
            // of the top 'N'

            int off = offset;
            int lim = limit >= 0 ? limit : Integer.MAX_VALUE;

            if (sort) {
                int maxsize = limit > 0 ? offset + limit : Integer.MAX_VALUE - 1;
                maxsize = Math.min(maxsize, nTerms);
                final TreeSet<CountPair<String, Integer>> queue = new TreeSet<CountPair<String, Integer>>();
                int min = mincount - 1; // the smallest value in the top 'N' values
                for (int i = 0; i < nTerms; i++) {
                    int c = counts[i];
                    if (c > min) {
                        // NOTE: we use c>min rather than c>=min as an optimization because we are going in
                        // index order, so we already know that the keys are ordered. This can be very
                        // important if a lot of the counts are repeated (like zero counts would be).
                        queue.add(new CountPair<String, Integer>(terms[startTermIndex + i], c));
                        if (queue.size() >= maxsize) {
                            min = queue.last().val;
                            break;
                        }
                    }
                }
                // now select the right page from the results
                for (CountPair<String, Integer> p : queue) {
                    if (--off >= 0)
                        continue;
                    if (--lim < 0)
                        break;
                    res.add(p.key, p.val);
                }
            } else {
                // add results in index order
                int i = 0;
                if (mincount <= 0) {
                    // if mincount<=0, then we won't discard any terms and we know exactly
                    // where to start.
                    i = off;
                    off = 0;
                }

                for (; i < nTerms; i++) {
                    int c = counts[i];
                    if (c < mincount || --off >= 0)
                        continue;
                    if (--lim < 0)
                        break;
                    res.add(terms[startTermIndex + i], c);
                }
            }
        }

        if (missing) {
            res.add(null, getFieldMissingCount(searcher, docs, fieldName, locale));
        }

        return res;
    }

    /**
     * Returns a list of terms in the specified field along with the corresponding count of documents in the set that match that constraint.
     * This method uses the FilterCache to get the intersection count between <code>docs</code> and the DocSet for each term in the filter.
     *
     * @see FacetParams#FACET_LIMIT
     * @see FacetParams#FACET_ZEROS
     * @see FacetParams#FACET_MISSING
     */
    public NamedList<Object> getFacetTermEnumCounts(IndexSearcher searcher, OpenBitSet docs,
            String field, String fieldName, int offset, int limit, int mincount, boolean missing,
            boolean sort, String prefix, String locale) throws IOException {

        /*
         * :TODO: potential optimization... cache the Terms with the highest docFreq and try them first don't enum if we get our max from
         * them
         */

        // Minimum term docFreq in order to use the filterCache for that term.
        int minDfFilterCache = params.getFieldInt(field, FacetParams.FACET_ENUM_CACHE_MINDF, 0);

        IndexReader r = searcher.getIndexReader();

        final int maxsize = limit >= 0 ? offset + limit : Integer.MAX_VALUE - 1;
        final TreeSet<CountPair<String, Integer>> queue = sort ? new TreeSet<CountPair<String, Integer>>()
                : null;
        final NamedList<Object> res = new NamedList<Object>();

        int min = mincount - 1; // the smallest value in the top 'N' values
        int off = offset;
        int lim = limit >= 0 ? limit : Integer.MAX_VALUE;

        String startTerm = prefix == null ? "" : prefix; // ft.toInternal(prefix);
        TermEnum te = r.terms(new Term(fieldName, startTerm));
        TermDocs td = r.termDocs();

        if (docs.size() >= mincount) {
            do {
                Term t = te.term();

                if (null == t || !t.field().equals(fieldName))
                    break;

                if (prefix != null && !t.text().startsWith(prefix))
                    break;

                int df = te.docFreq();

                // If we are sorting, we can use df>min (rather than >=) since we
                // are going in index order. For certain term distributions this can
                // make a large difference (for example, many terms with df=1).
                if (df > 0 && df > min) {
                    int c;

                    if (df >= minDfFilterCache) {
                        // use the filter cache
                        c = (int) OpenBitSet.intersectionCount(getDocIdSetForHits(searcher
                                .search(new TermQuery(t)), locale), docs);
                    } else {
                        // iterate over TermDocs to calculate the intersection
                        td.seek(te);
                        c = 0;
                        while (td.next()) {
                            int doc = td.doc();
                            if (locale != null) {
                                doc = getMainDocIdForTranslations(searcher.getIndexReader()
                                        .document(doc), locale);
                            }
                           
                            if (docs.fastGet(doc)) {
                                c++;
                            }
                        }
                    }

                    if (sort) {
                        if (c > min) {
                            queue.add(new CountPair<String, Integer>(t.text(), c));
                            if (queue.size() >= maxsize) {
                                min = queue.last().val;
                                break;
                            }
                        }
                    } else {
                        if (c >= mincount && --off < 0) {
                            if (--lim < 0)
                                break;
                            res.add(t.text(), c);
                        }
                    }
                }
            } while (te.next());
        }

        if (sort) {
            for (CountPair<String, Integer> p : queue) {
                if (--off >= 0)
                    continue;
                if (--lim < 0)
                    break;
                res.add(p.key, p.val);
            }
        }

        if (missing) {
            res.add(null, getFieldMissingCount(searcher, docs, fieldName, locale));
        }

        te.close();
        td.close();

        return res;
    }

    public String getFieldNameInIndex(String field, ExtendedPropertyDefinition epd, String langCode) {
        String fieldName = field;
        try {
            fieldName = resolver.getJCRName(NameFactoryImpl.getInstance().create(session.getNamespaceURI(epd.getPrefix()),
                    epd.getLocalName()));
            int idx = fieldName.indexOf(':');
            fieldName = fieldName.substring(0, idx + 1)
                    + (epd != null && epd.isFacetable() ? JahiaNodeIndexer.FACET_PREFIX
                            : FieldNames.FULLTEXT_PREFIX)
                    + fieldName.substring(idx + 1);
        } catch (RepositoryException e) {
            // will never happen
        }
        return fieldName;
    }

    /**
     * Returns a list of value constraints and the associated facet counts for each facet date field, range, and interval specified in the
     * SolrParams
     *
     * @see FacetParams#FACET_DATE
     */
    public NamedList<Object> getFacetDateCounts() throws JahiaException, IOException,
            RepositoryException {

        final SolrParams required = new RequiredSolrParams(params);
        final NamedList<Object> resOuter = new SimpleOrderedMap<Object>();
        final String[] fields = params.getParams(FacetParams.FACET_DATE);
        final Date NOW = new Date();

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

        for (String f : fields) {
            final NamedList<Object> resInner = new SimpleOrderedMap<Object>();
            String fieldName = StringUtils.substringBeforeLast(f, PROPNAME_INDEX_SEPARATOR);           
            ExtendedPropertyDefinition epd = NodeTypeRegistry.getInstance().getNodeType(params.get("f."+f+".facet.nodetype")).getPropertyDefinition(fieldName);
            String fieldNameInIndex = getFieldNameInIndex(fieldName, epd, params.getFieldParam(f,
                    "facet.locale"));
           
            resOuter.add(fieldName + PROPNAME_INDEX_SEPARATOR + fieldNameInIndex, resInner);
           
            if (!(epd.getRequiredType() == PropertyType.DATE)) {
                throw new JahiaException("Can not date facet on a field which is not a DateField: "
                        + f, "Can not date facet on a field which is not a DateField: " + f,
                        JahiaException.PARAMETER_ERROR, JahiaException.ERROR_SEVERITY);
            }
            Integer mincount = params.getFieldInt(f, FacetParams.FACET_MINCOUNT);
            if (mincount == null) {
                Boolean zeros = params.getFieldBool(f, FacetParams.FACET_ZEROS);
                // mincount = (zeros!=null && zeros) ? 0 : 1;
                mincount = (zeros != null && !zeros) ? 1 : 0;
                // current default is to include zeros.
            }
           
            final String startS = required.getFieldParam(f,
                    FacetParams.FACET_DATE_START);
            final Date start;
            try {
                start = JahiaQueryParser.DATE_TYPE.parseMath(NOW, startS);
            } catch (SolrException e) {
                throw new JahiaException(
                        "date facet 'start' is not a valid Date string: " + startS,
                        "date facet 'start' is not a valid Date string: " + startS,
                        JahiaException.PARAMETER_ERROR, JahiaException.ERROR_SEVERITY, e);
            }
            final String endS = required.getFieldParam(f, FacetParams.FACET_DATE_END);
            Date end; // not final, hardend may change this
            try {
                end = JahiaQueryParser.DATE_TYPE.parseMath(NOW, endS);
            } catch (SolrException e) {
                throw new JahiaException("date facet 'end' is not a valid Date string: " + endS,
                        "date facet 'end' is not a valid Date string: " + endS,
                        JahiaException.PARAMETER_ERROR, JahiaException.ERROR_SEVERITY, e);
            }

            if (end.before(start)) {
                throw new JahiaException("date facet 'end' comes before 'start': " + endS + " < "
                        + startS,
                        "date facet 'end' comes before 'start': " + endS + " < " + startS,
                        JahiaException.PARAMETER_ERROR, JahiaException.ERROR_SEVERITY);
            }

            final String gap = required.getFieldParam(f, FacetParams.FACET_DATE_GAP);
            final DateMathParser dmp = new DateMathParser(DateField.UTC, Locale.US);
            dmp.setNow(NOW);
            try {

                Date low = start;
                while (low.before(end)) {
                    dmp.setNow(low);
                    final String lowI = JahiaQueryParser.DATE_TYPE.toInternal(low);
                    final String label = JahiaQueryParser.DATE_TYPE.indexedToReadable(lowI, true);
                    Date high = dmp.parseMath(gap);
                    if (end.before(high)) {
                        if (params.getFieldBool(f, FacetParams.FACET_DATE_HARD_END,
                                false)) {
                            high = end;
                        } else {
                            end = high;
                        }
                    }
                    if (high.before(low)) {
                        throw new JahiaException("date facet infinite loop (is gap negative?)",
                                "date facet infinite loop (is gap negative?)",
                                JahiaException.PARAMETER_ERROR, JahiaException.ERROR_SEVERITY);
                    }
                    final String highI = JahiaQueryParser.DATE_TYPE.toInternal(high);
                    Query rangeQuery = getRangeQuery(fieldNameInIndex, lowI, highI, true, true);
                    int count = rangeCount(rangeQuery);
                    if (count >= mincount) {
                        resInner.add(label + PROPNAME_INDEX_SEPARATOR + rangeQuery.toString(),
                                count);
                    }
                    low = high;
                }
            } catch (java.text.ParseException e) {
                throw new JahiaException(
                        "date facet 'gap' is not a valid Date Math string: " + gap,
                        "date facet 'gap' is not a valid Date Math string: " + gap,
                        JahiaException.PARAMETER_ERROR, JahiaException.ERROR_SEVERITY, e);
            }

            // explicitly return the gap and end so all the counts are meaningful
            resInner.add("gap", gap);
            resInner.add("end", end);

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

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

                // no matter what other values are listed, we don't do
                // anything if "none" is specified.
                if (!others.contains(FacetDateOther.NONE)) {
                    final String startI = JahiaQueryParser.DATE_TYPE.toInternal(start);
                    final String endI = JahiaQueryParser.DATE_TYPE.toInternal(end);

                    boolean all = others.contains(FacetDateOther.ALL);

                    if (all || others.contains(FacetDateOther.BEFORE)) {
                        Query rangeQuery = getRangeQuery(fieldNameInIndex, null, startI, false,
                                false);
                        int count = rangeCount(rangeQuery);
                        if (count >= mincount) {
                            resInner.add(FacetDateOther.BEFORE.toString()
                                    + PROPNAME_INDEX_SEPARATOR + rangeQuery.toString(), count);
                        }
                    }
                    if (all || others.contains(FacetDateOther.AFTER)) {
                        Query rangeQuery = getRangeQuery(fieldNameInIndex, endI, null, false, false);
                        int count = rangeCount(rangeQuery);
                        if (count >= mincount) {
                            resInner.add(FacetDateOther.AFTER.toString() + PROPNAME_INDEX_SEPARATOR
                                    + rangeQuery.toString(), count);
                        }
                    }
                    if (all || others.contains(FacetDateOther.BETWEEN)) {
                        Query rangeQuery = getRangeQuery(fieldNameInIndex, startI, endI, true, true);
                        int count = rangeCount(rangeQuery);
                        if (count >= mincount) {
                            resInner.add(FacetDateOther.BETWEEN.toString()
                                    + PROPNAME_INDEX_SEPARATOR + rangeQuery.toString(), count);
                        }
                    }
                }
            }
        }

        return resOuter;
    }

    /**
     * Macro for getting the numDocs of a ConstantScoreRangeQuery over docs
     *
     * @see ConstantScoreRangeQuery
     */
    protected int rangeCount(Query rangeQuery) throws IOException {
        return (int) OpenBitSet.intersectionCount(getDocIdSetForHits(searcher.search(rangeQuery), null),
                docs);
    }

    protected Query getRangeQuery(String field, String low, String high, boolean iLow, boolean iHigh) {
        return new ConstantScoreRangeQuery(field, low, high, iLow, iHigh);
    }

    /**
     * A simple key=>val pair whose natural order is such that <b>higher</b> vals come before lower vals. In case of tie vals, then
     * <b>lower</b> keys come before higher keys.
     */
    public static class CountPair<K extends Comparable<? super K>, V extends Comparable<? super V>>
            implements Comparable<CountPair<K, V>> {

        public CountPair(K k, V v) {
            key = k;
            val = v;
        }

        public K key;
        public V val;

        public int hashCode() {
            return key.hashCode() ^ val.hashCode();
        }

        public boolean equals(Object o) {
            return (o instanceof CountPair) && (0 == this.compareTo((CountPair<K, V>) o));
        }

        public int compareTo(CountPair<K, V> o) {
            int vc = o.val.compareTo(val);
            return (0 != vc ? vc : key.compareTo(o.key));
        }
    }

    private OpenBitSet getDocIdSetForHits(Hits hits, String locale) {
        OpenBitSet docIds = null;
        try {
            BitSet bitset = new BitSet();
            int docId;
            for (Iterator<Hit> it = hits.iterator(); it.hasNext();) {
                if (locale == null) {
                    docId = it.next().getId();
                } else {
                    Hit hit = it.next();
                    Document doc = hit.getDocument();
                    docId = getMainDocIdForTranslations(doc, locale);
                    if (docId == -1) {
                        docId = hit.getId();
                    } else {
                        bitset.set(hit.getId());                       
                    }
                }
                bitset.set(docId);
            }
            docIds = new OpenBitSetDISI(new DocIdBitSet(bitset).iterator(), bitset.size());
        } catch (IOException e) {
            logger.debug("Can't retrive bitset from hits", e);
        }
        return docIds;
    }

    private int getMainDocIdForTranslations(Document doc, String locale) {
        int docId = -1;
        Field parentNode = doc.getField(JahiaNodeIndexer.TRANSLATED_NODE_PARENT);
        if (parentNode != null
                && (StringUtils.isEmpty(locale) || locale.equals(doc.getField(
                        JahiaNodeIndexer.TRANSLATION_LANGUAGE).stringValue()))) {
            try {
                String id = doc.getField(FieldNames.PARENT).stringValue();
                TermDocs docs = searcher.getIndexReader().termDocs(new Term(FieldNames.UUID, id));
                try {
                    if (docs.next()) {
                        return docs.doc();
                    } else {
                        throw new IOException("Node with id " + id + " not found in index");
                    }
                } finally {
                    docs.close();
                }               
            } catch (IOException e) {
                logger.debug("Can't retrive parent node of translation node", e);
            }
        }
        return docId;
    }
}
TOP

Related Classes of org.jahia.services.search.facets.SimpleJahiaJcrFacets

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.