Java tutorial
/* * Copyright (C) 2012 sol-dock-r 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 at.pagu.soldockr.core.query; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.util.Assert; import at.pagu.soldockr.ApiUsageException; import at.pagu.soldockr.core.convert.DateTimeConverters; import at.pagu.soldockr.core.convert.NumberConverters; /** * Criteria is the central class when constructing queries. * It follows more or less a fluent API style, which allows to easily chain together multiple criteria. * * @author Christoph Strobl */ public class Criteria implements QueryStringHolder { public static final String WILDCARD = "*"; public static final String CRITERIA_VALUE_SEPERATOR = " "; private static final String OR_OPERATOR = " OR "; private static final String DELIMINATOR = ":"; private static final String AND_OPERATOR = " AND "; private static final String RANGE_OPERATOR = " TO "; private static final String DOUBLEQUOTE = "\""; private static final String[] RESERVED_CHARS = { DOUBLEQUOTE, "+", "-", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "~", "*", "?", ":", "\\" }; private static final String[] RESERVED_CHARS_REPLACEMENT = { "\\" + DOUBLEQUOTE, "\\+", "\\-", "\\&\\&", "\\|\\|", "\\!", "\\(", "\\)", "\\{", "\\}", "\\[", "\\]", "\\^", "\\~", "\\*", "\\?", "\\:", "\\\\" }; private final GenericConversionService conversionService = new GenericConversionService(); private Field field; private float boost = Float.NaN; private List<Criteria> criteriaChain = new ArrayList<Criteria>(1); private Set<CriteriaEntry> criteria = new LinkedHashSet<CriteriaEntry>(); { if (!conversionService.canConvert(java.util.Date.class, String.class)) { conversionService.addConverter(DateTimeConverters.JavaDateConverter.INSTANCE); } if (!conversionService.canConvert(org.joda.time.ReadableInstant.class, String.class)) { conversionService.addConverter(DateTimeConverters.JodaDateTimeConverter.INSTANCE); } if (!conversionService.canConvert(org.joda.time.LocalDateTime.class, String.class)) { conversionService.addConverter(DateTimeConverters.JodaLocalDateTimeConverter.INSTANCE); } if (!conversionService.canConvert(Number.class, String.class)) { conversionService.addConverter(NumberConverters.NumberConverter.INSTANCE); } } public Criteria() { } /** * Creates a new Criteria for the Filed with provided name * * @param fieldname */ public Criteria(String fieldname) { this(new SimpleField(fieldname)); } /** * Creates a new Criteria for the given field * * @param field */ public Criteria(Field field) { Assert.notNull(field, "Field for criteria must not be null"); Assert.hasText(field.getName(), "Field.name for criteria must not be null/empty"); this.criteriaChain.add(this); this.field = field; } protected Criteria(List<Criteria> criteriaChain, String fieldname) { this(criteriaChain, new SimpleField(fieldname)); } protected Criteria(List<Criteria> criteriaChain, Field field) { Assert.notNull(criteriaChain, "CriteriaChain must not be null"); Assert.notNull(field, "Field for criteria must not be null"); Assert.hasText(field.getName(), "Field.name for criteria must not be null/empty"); this.criteriaChain.addAll(criteriaChain); this.criteriaChain.add(this); this.field = field; } /** * Static factory method to create a new Criteria for field with given name * * @param field * @return */ public static Criteria where(String field) { return where(new SimpleField(field)); } /** * Static factory method to create a new Criteria for provided field * * @param field * @return */ public static Criteria where(Field field) { return new Criteria(field); } /** * Chain using AND * * @param field * @return */ public Criteria and(Field field) { return new Criteria(this.criteriaChain, field); } /** * Chain using AND * * @param field * @return */ public Criteria and(String fieldname) { return new Criteria(this.criteriaChain, fieldname); } /** * Chain using AND * * @param field * @return */ public Criteria and(Criteria criteria) { this.criteriaChain.add(criteria); return this; } /** * Chain using AND * * @param field * @return */ public Criteria and(Criteria... criterias) { this.criteriaChain.addAll(Arrays.asList(criterias)); return this; } /** * Chain using OR * * @param field * @return */ public Criteria or(Field field) { return new OrCriteria(this.criteriaChain, field); } /** * Chain using OR * * @param field * @return */ public Criteria or(Criteria criteria) { Assert.notNull(criteria, "Cannot chain 'null' criteria."); Criteria orConnectedCritiera = new OrCriteria(this.criteriaChain, criteria.getField()); orConnectedCritiera.criteria.addAll(criteria.criteria); return orConnectedCritiera; } /** * Chain using OR * * @param field * @return */ public Criteria or(String fieldname) { return or(new SimpleField(fieldname)); } /** * Crates new CriteriaEntry without any wildcards * * @param o * @return */ public Criteria is(Object o) { criteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); return this; } /** * Crates new CriteriaEntry with leading and trailing wildcards * * @param o * @return */ public Criteria contains(String s) { assertNoBlankInWildcardedQuery(s, true, true); criteria.add(new CriteriaEntry(OperationKey.CONTAINS, s)); return this; } /** * Crates new CriteriaEntry with leading wildcard * * @param o * @return */ public Criteria startsWith(String s) { assertNoBlankInWildcardedQuery(s, true, false); criteria.add(new CriteriaEntry(OperationKey.STARTS_WITH, s)); return this; } /** * Crates new CriteriaEntry with trailing wildcards * * @param o * @return */ public Criteria endsWith(String s) { assertNoBlankInWildcardedQuery(s, false, true); criteria.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); return this; } /** * Crates new CriteriaEntry with trailing - * * @param s * @return */ public Criteria isNot(Object o) { criteria.add(new CriteriaEntry(OperationKey.IS_NOT, o)); return this; } /** * Crates new CriteriaEntry with trailing ~ * * @param s * @return */ public Criteria fuzzy(String s) { return fuzzy(s, Float.NaN); } /** * Crates new CriteriaEntry with trailing ~ followed by levensteinDistance * * @param s * @param levenshteinDistance * @return */ public Criteria fuzzy(String s, float levenshteinDistance) { if (!Float.isNaN(levenshteinDistance)) { if (levenshteinDistance < 0 || levenshteinDistance > 1) { throw new ApiUsageException("Levenshtein Distance has to be within its bounds (0.0 - 1.0)."); } } criteria.add(new CriteriaEntry("$fuzzy#" + levenshteinDistance, s)); return this; } /** * Crates new CriteriaEntry allowing native solr expressions * * @param o * @return */ public Criteria expression(String s) { criteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); return this; } /** * Boost positive hit with given factor. eg. ^2.3 * * @param boost * @return */ public Criteria boost(float boost) { if (boost < 0) { throw new ApiUsageException("Boost must not be negative."); } this.boost = boost; return this; } /** * Crates new CriteriaEntry for RANGE expressions [lowerBound TO upperBound] * * @param lowerBound * @param upperBound * @return */ public Criteria between(Object lowerBound, Object upperBound) { if (lowerBound == null && upperBound == null) { throw new ApiUsageException("Range [* TO *] is not allowed"); } criteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); return this; } /** * Crates new CriteriaEntry for RANGE [* TO upperBound] * * @param upperBound * @return */ public Criteria lessThanEqual(Object upperBound) { between(null, upperBound); return this; } /** * Crates new CriteriaEntry for RANGE [lowerBound TO *] * * @param lowerBound * @return */ public Criteria greaterThanEqual(Object lowerBound) { between(lowerBound, null); return this; } /** * Crates new CriteriaEntry for multiple values (arg0 arg1 arg2 ...) * * @param lowerBound * @return */ public Criteria in(Object... values) { if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { throw new ApiUsageException("At least one element " + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") + " has to be present."); } return in(Arrays.asList(values)); } /** * Crates new CriteriaEntry for multiple values (arg0 arg1 arg2 ...) * * @param c the collection containing the values to match against * @return */ public Criteria in(Iterable<?> values) { Assert.notNull(values, "Collection of 'in' values must not be null"); for (Object value : values) { if (value instanceof Collection) { in((Collection<?>) value); } else { is(value); } } return this; } /** * get the QueryString used for executing query * * @return */ public String createQueryString() { StringBuilder query = new StringBuilder(StringUtils.EMPTY); ListIterator<Criteria> chainIterator = this.criteriaChain.listIterator(); while (chainIterator.hasNext()) { Criteria chainedCriteria = chainIterator.next(); query.append(createQueryFragmentForCriteria(chainedCriteria)); if (chainIterator.hasNext()) { query.append(chainIterator.next().getConjunctionOperator()); chainIterator.previous(); } } return query.toString(); } protected String createQueryFragmentForCriteria(Criteria chainedCriteria) { StringBuilder queryFragment = new StringBuilder(); Iterator<CriteriaEntry> it = chainedCriteria.criteria.iterator(); boolean singeEntryCriteria = (chainedCriteria.criteria.size() == 1); if (chainedCriteria.field != null) { queryFragment.append(chainedCriteria.field.getName()); queryFragment.append(DELIMINATOR); if (!singeEntryCriteria) { queryFragment.append("("); } while (it.hasNext()) { CriteriaEntry entry = it.next(); queryFragment.append(processCriteriaEntry(entry.getKey(), entry.getValue())); if (it.hasNext()) { queryFragment.append(CRITERIA_VALUE_SEPERATOR); } } if (!singeEntryCriteria) { queryFragment.append(")"); } if (!Float.isNaN(chainedCriteria.boost)) { queryFragment.append("^" + chainedCriteria.boost); } } else { return chainedCriteria.getQueryString(); } return queryFragment.toString(); } public String getQueryString() { return field != null ? createQueryString() : ""; } private String processCriteriaEntry(String key, Object value) { if (value == null) { return null; } // do not filter espressions if (StringUtils.equals(OperationKey.EXPRESSION.getKey(), key)) { return value.toString(); } if (StringUtils.equals(OperationKey.BETWEEN.getKey(), key)) { Object[] args = (Object[]) value; String rangeFragment = "["; rangeFragment += args[0] != null ? filterCriteriaValue(args[0]) : WILDCARD; rangeFragment += RANGE_OPERATOR; rangeFragment += args[1] != null ? filterCriteriaValue(args[1]) : WILDCARD; rangeFragment += "]"; return rangeFragment; } Object filteredValue = filterCriteriaValue(value); if (StringUtils.equals(OperationKey.CONTAINS.getKey(), key)) { return WILDCARD + filteredValue + WILDCARD; } if (StringUtils.equals(OperationKey.STARTS_WITH.getKey(), key)) { return filteredValue + WILDCARD; } if (StringUtils.equals(OperationKey.ENDS_WITH.getKey(), key)) { return WILDCARD + filteredValue; } if (StringUtils.equals(OperationKey.IS_NOT.getKey(), key)) { return "-" + filteredValue; } if (StringUtils.startsWith(key, "$fuzzy")) { String sDistance = StringUtils.substringAfter(key, "$fuzzy#"); float distance = Float.NaN; if (StringUtils.isNotBlank(sDistance)) { distance = Float.parseFloat(sDistance); } return filteredValue + "~" + (Float.isNaN(distance) ? "" : sDistance); } return filteredValue.toString(); } private Object filterCriteriaValue(Object criteriaValue) { if (!(criteriaValue instanceof String)) { if (conversionService.canConvert(criteriaValue.getClass(), String.class)) { return conversionService.convert(criteriaValue, String.class); } return criteriaValue; } String value = escapeCriteriaValue((String) criteriaValue); return processWhiteSpaces(value); } private String escapeCriteriaValue(String criteriaValue) { return StringUtils.replaceEach(criteriaValue, RESERVED_CHARS, RESERVED_CHARS_REPLACEMENT); } private String processWhiteSpaces(String criteriaValue) { if (StringUtils.contains(criteriaValue, CRITERIA_VALUE_SEPERATOR)) { return DOUBLEQUOTE + criteriaValue + DOUBLEQUOTE; } return criteriaValue; } private void assertNoBlankInWildcardedQuery(String searchString, boolean leadingWildcard, boolean trailingWildcard) { if (StringUtils.contains(searchString, CRITERIA_VALUE_SEPERATOR)) { throw new ApiUsageException( "Cannot constructQuery '" + (leadingWildcard ? "*" : "") + "\"" + searchString + "\"" + (trailingWildcard ? "*" : "") + "'. Use epxression or mulitple clauses instead."); } } /** * Field targeted by this Criteria * @return */ public Field getField() { return this.field; } /** * Conjunction to be used with this criteria (AND | OR) * @return */ public String getConjunctionOperator() { return AND_OPERATOR; } List<Criteria> getCriteriaChain() { return this.criteriaChain; } /** * Register an additional converter for transforming object values to solr readable format * @param converter */ public void registerConverter(Converter<?, ?> converter) { conversionService.addConverter(converter); } static class OrCriteria extends Criteria { public OrCriteria() { super(); } public OrCriteria(Field field) { super(field); } public OrCriteria(List<Criteria> criteriaChain, Field field) { super(criteriaChain, field); } public OrCriteria(List<Criteria> criteriaChain, String fieldname) { super(criteriaChain, fieldname); } public OrCriteria(String fieldname) { super(fieldname); } @Override public String getConjunctionOperator() { return OR_OPERATOR; } } enum OperationKey { IS_NOT("$isNot"), EQUALS("$equals"), CONTAINS("$contains"), STARTS_WITH("$startsWith"), ENDS_WITH( "$endsWith"), EXPRESSION("$expression"), BETWEEN("$between"); private final String key; private OperationKey(String key) { this.key = key; } public String getKey() { return this.key; } } static class CriteriaEntry { private String key; private Object value; CriteriaEntry(OperationKey key, Object value) { this(key.getKey(), value); } CriteriaEntry(String key, Object value) { this.key = key; this.value = value; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } } }