/* * Copyright 2012 - 2017 the original author or authors. * * 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.springframework.data.solr.core.query; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import org.apache.solr.common.params.FacetParams; import org.apache.solr.common.params.FacetParams.FacetRangeInclude; import org.apache.solr.common.params.FacetParams.FacetRangeOther; import org.springframework.data.domain.Pageable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Set of options that can be set on a {@link FacetQuery} * * @author Christoph Strobl * @author Francisco Spaeth */ public class FacetOptions { public static final int DEFAULT_FACET_MIN_COUNT = 1; public static final int DEFAULT_FACET_LIMIT = 10; public static final FacetSort DEFAULT_FACET_SORT = FacetSort.COUNT; public enum FacetSort { COUNT, INDEX } private List<Field> facetOnFields = new ArrayList<>(1); private List<PivotField> facetOnPivotFields = new ArrayList<>(0); private List<FieldWithRangeParameters<?, ?, ?>> facetRangeOnFields = new ArrayList<>(1); private List<SolrDataQuery> facetQueries = new ArrayList<>(0); private int facetMinCount = DEFAULT_FACET_MIN_COUNT; private int facetLimit = DEFAULT_FACET_LIMIT; private String facetPrefix; private FacetSort facetSort = DEFAULT_FACET_SORT; private Pageable pageable; public FacetOptions() {} /** * Creates new instance faceting on fields with given name * * @param fieldnames */ public FacetOptions(String... fieldnames) { Assert.notNull(fieldnames, "Fields must not be null."); Assert.noNullElements(fieldnames, "Cannot facet on null fieldname."); for (String fieldname : fieldnames) { addFacetOnField(fieldname); } } /** * Creates new instance faceting on given fields */ public FacetOptions(Field... fields) { Assert.notNull(fields, "Fields must not be null."); Assert.noNullElements(fields, "Cannot facet on null field."); for (Field field : fields) { addFacetOnField(field); } } /** * Creates new instance faceting on given queries * * @param facetQueries */ public FacetOptions(SolrDataQuery... facetQueries) { Assert.notNull(facetQueries, "Facet Queries must not be null."); Assert.noNullElements(facetQueries, "Cannot facet on null query."); this.facetQueries.addAll(Arrays.asList(facetQueries)); } /** * Append additional field for faceting * * @param field * @return */ public final FacetOptions addFacetOnField(Field field) { Assert.notNull(field, "Cannot facet on null field."); Assert.hasText(field.getName(), "Cannot facet on field with null/empty fieldname."); this.facetOnFields.add(field); return this; } /** * Append additional field with given name for faceting * * @param fieldname * @return */ public final FacetOptions addFacetOnField(String fieldname) { addFacetOnField(new SimpleField(fieldname)); return this; } /** * Append additional field for range faceting * * @param field the {@link Field} to be appended to range faceting fields * @return this * @since 1.5 */ public final FacetOptions addFacetByRange(FieldWithRangeParameters<?, ?, ?> field) { Assert.notNull(field, "Cannot range facet on null field."); Assert.hasText(field.getName(), "Cannot range facet on field with null/empty fieldname."); this.facetRangeOnFields.add(field); return this; } /** * Add pivot facet on given {@link Field}s. * * @param fields * @return */ public final FacetOptions addFacetOnPivot(Field... fields) { Assert.notNull(fields, "Pivot Facets must not be null."); for (Field field : fields) { Assert.notNull(field, "Cannot facet on null field."); Assert.hasText(field.getName(), "Cannot facet on field with null/empty fieldname."); } List<Field> list = Arrays.asList(fields); this.facetOnPivotFields.add(new SimplePivotField(list)); return this; } /** * @return */ public final FacetOptions addFacetOnPivot(String... fieldnames) { Assert.state(fieldnames.length > 1, "2 or more fields required for pivot facets"); for (String fieldname : fieldnames) { Assert.hasText(fieldname, "Fieldnames must not contain null/empty values"); } this.facetOnPivotFields.add(new SimplePivotField(fieldnames)); return this; } /** * Append all fieldnames for faceting * * @param fieldnames * @return */ public final FacetOptions addFacetOnFlieldnames(Collection<String> fieldnames) { Assert.notNull(fieldnames, "Fieldnames must not be null!"); for (String fieldname : fieldnames) { addFacetOnField(fieldname); } return this; } /** * Append {@code facet.query} * * @param query * @return */ public final FacetOptions addFacetQuery(SolrDataQuery query) { Assert.notNull(query, "Facet Query must not be null."); this.facetQueries.add(query); return this; } /** * Get the list of facetQueries * * @return */ public List<SolrDataQuery> getFacetQueries() { return Collections.unmodifiableList(this.facetQueries); } /** * Set minimum number of hits {@code facet.mincount} for result to be included in response * * @param minCount Default is 1 * @return */ public FacetOptions setFacetMinCount(int minCount) { this.facetMinCount = Math.max(0, minCount); return this; } /** * Set {@code facet.limit} * * @param rowsToReturn Default is 10 * @return */ public FacetOptions setFacetLimit(int rowsToReturn) { this.facetLimit = rowsToReturn; return this; } /** * Set {@code facet.sort} ({@code INDEX} or {@code COUNT}) * * @param facetSort Default is {@code COUNT} * @return */ public FacetOptions setFacetSort(FacetSort facetSort) { Assert.notNull(facetSort, "FacetSort must not be null."); this.facetSort = facetSort; return this; } /** * Get the list of Fields to facet on * * @return */ public final List<Field> getFacetOnFields() { return Collections.unmodifiableList(this.facetOnFields); } /** * Get the list of pivot Fields to face on * * @return */ public final List<PivotField> getFacetOnPivots() { return Collections.unmodifiableList(facetOnPivotFields); } /** * get the min number of hits a result has to have to get listed in result. Default is 1. Zero is not recommended. * * @return */ public int getFacetMinCount() { return this.facetMinCount; } /** * Get the max number of results per facet field. * * @return */ public int getFacetLimit() { return this.facetLimit; } /** * Get sorting of facet results. Default is COUNT * * @return */ public FacetSort getFacetSort() { return this.facetSort; } /** * Get the facet page requested. * * @return */ public Pageable getPageable() { return this.pageable != null ? this.pageable : new SolrPageRequest(0, facetLimit); } /** * Set {@code facet.offet} and {@code facet.limit} * * @param pageable * @return */ public FacetOptions setPageable(Pageable pageable) { this.pageable = pageable; return this; } /** * get value used for {@code facet.prefix} * * @return */ public String getFacetPrefix() { return facetPrefix; } /** * Set {@code facet.prefix} * * @param facetPrefix * @return */ public FacetOptions setFacetPrefix(String facetPrefix) { this.facetPrefix = facetPrefix; return this; } /** * @return true if at least one facet field set */ public boolean hasFields() { return !this.facetOnFields.isEmpty() || !this.facetOnPivotFields.isEmpty(); } /** * @return true if filter queries applied for faceting */ public boolean hasFacetQueries() { return !this.facetQueries.isEmpty(); } /** * @return true if pivot facets apply fo faceting */ public boolean hasPivotFields() { return !facetOnPivotFields.isEmpty(); } private boolean hasFacetRages() { return !facetRangeOnFields.isEmpty(); } /** * @return true if any {@code facet.field} or {@code facet.query} set */ public boolean hasFacets() { return hasFields() || hasFacetQueries() || hasPivotFields() || hasFacetRages(); } /** * @return true if non empty prefix available */ public boolean hasFacetPrefix() { return StringUtils.hasText(this.facetPrefix); } @SuppressWarnings("unchecked") public Collection<FieldWithFacetParameters> getFieldsWithParameters() { List<FieldWithFacetParameters> result = new ArrayList<>(); for (Field candidate : facetOnFields) { if (candidate instanceof FieldWithFacetParameters) { result.add((FieldWithFacetParameters) candidate); } } return result; } public static class FacetParameter extends QueryParameterImpl { public FacetParameter(String parameter, Object value) { super(parameter, value); } } public static class FieldWithFacetParameters extends FieldWithQueryParameters<FacetParameter> { private FacetSort sort; public FieldWithFacetParameters(String name) { super(name); } /** * @param prefix */ public FieldWithFacetParameters setPrefix(String prefix) { addFacetParameter(FacetParams.FACET_PREFIX, prefix); return this; } /** * @return null if not set */ public String getPrefix() { return getQueryParameterValue(FacetParams.FACET_PREFIX); } /** * @param sort */ public FieldWithFacetParameters setSort(FacetSort sort) { this.sort = sort; return this; } /** * @return null if not set */ public FacetSort getSort() { return this.sort; } /** * @param limit */ public FieldWithFacetParameters setLimit(Integer limit) { addFacetParameter(FacetParams.FACET_LIMIT, Math.max(0, limit), true); return this; } /** * @return null if not set */ public Integer getLimit() { return getQueryParameterValue(FacetParams.FACET_LIMIT); } /** * @param offset */ public FieldWithFacetParameters setOffset(Integer offset) { addFacetParameter(FacetParams.FACET_OFFSET, offset, true); return this; } /** * @return null if not set */ public Integer getOffset() { return getQueryParameterValue(FacetParams.FACET_OFFSET); } /** * @param minCount */ public FieldWithFacetParameters setMinCount(Integer minCount) { addFacetParameter(FacetParams.FACET_MINCOUNT, Math.max(0, minCount), true); return this; } /** * @return null if not set */ public Integer getMinCount() { return getQueryParameterValue(FacetParams.FACET_MINCOUNT); } /** * @param missing * @return */ public FieldWithFacetParameters setMissing(Boolean missing) { addFacetParameter(FacetParams.FACET_MISSING, missing, true); return this; } /** * @return null if not set */ public Boolean getMissing() { return getQueryParameterValue(FacetParams.FACET_MISSING); } /** * @param method * @return */ public FieldWithFacetParameters setMethod(String method) { addFacetParameter(FacetParams.FACET_METHOD, method, true); return this; } /** * @return null if not set */ public String getMethod() { return getQueryParameterValue(FacetParams.FACET_METHOD); } /** * Add field specific parameter by name * * @param parameterName * @param value */ public FieldWithFacetParameters addFacetParameter(String parameterName, Object value) { return addFacetParameter(parameterName, value, false); } protected FieldWithFacetParameters addFacetParameter(String parameterName, Object value, boolean removeIfValueIsNull) { if (removeIfValueIsNull && value == null) { removeQueryParameter(parameterName); return this; } return this.addFacetParameter(new FacetParameter(parameterName, value)); } /** * Add field specific facet parameter * * @param parameter * @return */ public FieldWithFacetParameters addFacetParameter(FacetParameter parameter) { this.addQueryParameter(parameter); return this; } } /** * @return * @since 1.5 */ public Collection<FieldWithRangeParameters<?, ?, ?>> getFieldsWithRangeParameters() { return Collections.unmodifiableCollection(facetRangeOnFields); } /** * Class representing common facet range parameters. * * @author Francisco Spaeth * @param <T> range field implementation type * @param <R> type of range * @param <G> type of gap * @since 1.5 */ public abstract static class FieldWithRangeParameters<T extends FieldWithRangeParameters<?, ?, ?>, R, G> extends FieldWithQueryParameters<FacetParameter> { /** * @param name field name * @param start range facet start * @param end range facet end * @param gap gap to be used for faceting between start and end */ public FieldWithRangeParameters(String name, R start, R end, G gap) { super(name); Assert.notNull(start, "date range facet start must not be null for field " + name); Assert.notNull(end, "date range facet end must not be null for field " + name); Assert.notNull(gap, "date range facet gap must not be null for field" + gap); addFacetRangeParameter(FacetParams.FACET_RANGE, name); addFacetRangeParameter(FacetParams.FACET_RANGE_START, start); addFacetRangeParameter(FacetParams.FACET_RANGE_END, end); addFacetRangeParameter(FacetParams.FACET_RANGE_GAP, gap); } /** * Defines if the last range should be abruptly ended even if the end doesn't satisfies: (start - end) % gap = 0. * * @param rangeHardEnd whenever <code>false</code> will expect to have the last range with the same size as the * other ranges entries for the query, otherwise (<code>true</code>), may present the last range smaller * than the other range entries. * @return this * @see FacetParams#FACET_RANGE_HARD_END */ @SuppressWarnings("unchecked") public T setHardEnd(Boolean rangeHardEnd) { addFacetRangeParameter(FacetParams.FACET_RANGE_HARD_END, rangeHardEnd); return (T) this; } /** * If the last range should be abruptly ended even if the end doesn't satisfies: (start - end) % gap = 0. * * @return if hard end should be used, <code>null</code> will be returned if not set * @see FacetParams#FACET_RANGE_HARD_END */ public Boolean getHardEnd() { return getQueryParameterValue(FacetParams.FACET_RANGE_HARD_END); } /** * Defines the additional (other) counts for the range facet, i.e. count of documents that are before start of the * range facet, end of range facet or even between start and end. * * @param rangeOther which other counts shall be added to the facet result * @return this * @see FacetParams.FACET_RANGE_OTHER */ @SuppressWarnings("unchecked") public T setOther(FacetParams.FacetRangeOther rangeOther) { addFacetRangeParameter(FacetParams.FACET_RANGE_OTHER, rangeOther); return (T) this; } /** * The definition of additional (other) counts for the range facet. * * @return null which other counts shall be added to the facet result * @see FacetParams.FACET_RANGE_OTHER */ public FacetRangeOther getOther() { return getQueryParameterValue(FacetParams.FACET_RANGE_OTHER); } /** * Defines how boundaries (lower and upper) shall be handled (exclusive or inclusive) on range facet requests. * * @param rangeInclude include option for range * @return this * @see FacetParams.FACET_RANGE_INCLUDE */ @SuppressWarnings("unchecked") public T setInclude(FacetParams.FacetRangeInclude rangeInclude) { addFacetRangeParameter(FacetParams.FACET_RANGE_INCLUDE, rangeInclude); return (T) this; } /** * The definition of how boundaries (lower and upper) shall be handled (exclusive or inclusive) on range facet * requests. * * @return null if not set * @see FacetParams.FACET_RANGE_INCLUDE */ public FacetRangeInclude getInclude() { return getQueryParameterValue(FacetParams.FACET_RANGE_INCLUDE); } @SuppressWarnings("unchecked") protected T addFacetRangeParameter(String parameterName, Object value) { if (value == null) { removeQueryParameter(parameterName); } else { addQueryParameter(new FacetParameter(parameterName, value)); } return (T) this; } /** * The size of the range to be added to the lower bound. * * @return size of each range. * @see FacetParams#FACET_RANGE_GAP */ public G getGap() { return getQueryParameterValue(FacetParams.FACET_RANGE_GAP); } /** * Start value configured for this field range facet. * * @return upper bound for the ranges. * @see FacetParams#FACET_RANGE_START */ public R getStart() { return getQueryParameterValue(FacetParams.FACET_RANGE_START); } /** * @return lower bound for the ranges. * @see FacetParams#FACET_RANGE_END */ public R getEnd() { return getQueryParameterValue(FacetParams.FACET_RANGE_END); } } /** * Class representing date field specific facet range parameters * * @author Francisco Spaeth * @since 1.5 */ public static class FieldWithDateRangeParameters extends FieldWithRangeParameters<FieldWithDateRangeParameters, Date, String> { public FieldWithDateRangeParameters(String name, Date start, Date end, String gap) { super(name, start, end, gap); } } /** * Class representing numeric field specific facet range parameters * * @author Francisco Spaeth * @since 1.5 */ public static class FieldWithNumericRangeParameters extends FieldWithRangeParameters<FieldWithNumericRangeParameters, Number, Number> { public FieldWithNumericRangeParameters(String name, Number start, Number end, Number gap) { super(name, start, end, gap); } } }