package org.gbif.occurrence.download.service; import org.gbif.api.model.occurrence.predicate.ConjunctionPredicate; import org.gbif.api.model.occurrence.predicate.DisjunctionPredicate; import org.gbif.api.model.occurrence.predicate.EqualsPredicate; import org.gbif.api.model.occurrence.predicate.GreaterThanOrEqualsPredicate; import org.gbif.api.model.occurrence.predicate.IsNotNullPredicate; import org.gbif.api.model.occurrence.predicate.LessThanOrEqualsPredicate; import org.gbif.api.model.occurrence.predicate.Predicate; import org.gbif.api.model.occurrence.predicate.WithinPredicate; import org.gbif.api.model.occurrence.search.OccurrenceSearchParameter; import org.gbif.api.util.SearchTypeValidator; import org.gbif.api.util.VocabularyUtils; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import com.google.common.collect.Lists; import com.google.common.collect.Range; /** * Utility for dealing with the decoding of the request parameters to the * query object to pass into the service. * This parses the URL params which should be from something like the following * into a predicate suitable for launching a download service. * It understands multi valued parameters and interprets the range format *,100 * {@literal TAXON_KEY=12&ELEVATION=1000,2000 * (ELEVATION >= 1000 AND ELEVATION <= 1000)} */ public class PredicateFactory { private static final String POLYGON = "POLYGON((%s))"; private static final String WILDCARD = "*"; /** * Making constructor private. */ private PredicateFactory() { //empty constructor } /** * Builds a full predicate filter from the parameters. * In case no filters exist still return a predicate that matches anything. * * @return always some predicate */ public static Predicate build(Map<String, String[]> params) { // predicates for different parameters. If multiple values for the same parameter exist these are in here already List<Predicate> groupedByParam = Lists.newArrayList(); for (Map.Entry<String,String[]> p : params.entrySet()) { // recognize valid params by enum name, ignore others OccurrenceSearchParameter param = toEnumParam(p.getKey()); if (param != null) { // valid parameter Predicate predicate = buildParamPredicate(param, p.getValue()); if (predicate != null) { groupedByParam.add(predicate); } } } if (groupedByParam.isEmpty()) { // no filter at all return null; } else if (groupedByParam.size() == 1) { return groupedByParam.get(0); } else { // AND the individual params return new ConjunctionPredicate(groupedByParam); } } /** * @param name * @return the search enum or null if it cant be converted */ private static OccurrenceSearchParameter toEnumParam(String name) { try { return (OccurrenceSearchParameter) VocabularyUtils.lookupEnum(name, OccurrenceSearchParameter.class); } catch (IllegalArgumentException e) { return null; } } private static Predicate buildParamPredicate(OccurrenceSearchParameter param, String... values) { List<Predicate> predicates = Lists.newArrayList(); for (String v : values) { Predicate p = parsePredicate(param, v); if (p != null) { predicates.add(p); } } if (predicates.isEmpty()) { return null; } else if (predicates.size() == 1) { return predicates.get(0); } else { // OR the individual params return new DisjunctionPredicate(predicates); } } private static String toIsoDate(Date d) { return new SimpleDateFormat("yyyy-MM-dd").format(d); } /** * Converts a value with an optional predicate prefix into a real predicate instance, defaulting to EQUALS. */ private static Predicate parsePredicate(OccurrenceSearchParameter param, String value) { // geometry filters are special if (OccurrenceSearchParameter.GEOMETRY == param) { return new WithinPredicate(String.format(POLYGON, value)); } // test for ranges if (SearchTypeValidator.isRange(value)) { Range<?> range; if (Double.class.equals(param.type())) { range = SearchTypeValidator.parseDecimalRange(value); } else if (Integer.class.equals(param.type())) { range = SearchTypeValidator.parseIntegerRange(value); } else if (Date.class.equals(param.type())) { range = SearchTypeValidator.parseDateRange(value); // convert date instances back to strings, but keep the new precision which is now always up to the day! range = SearchTypeValidator.buildRange( range.hasLowerBound() ? toIsoDate((Date) range.lowerEndpoint()) : null, range.hasUpperBound() ? toIsoDate((Date) range.upperEndpoint()) : null ); } else { throw new IllegalArgumentException( "Ranges are only supported for numeric or date parameter types but received " + param); } List<Predicate> rangePredicates = Lists.newArrayList(); if (range.hasLowerBound()) { rangePredicates.add(new GreaterThanOrEqualsPredicate(param, range.lowerEndpoint().toString())); } if (range.hasUpperBound()) { rangePredicates.add(new LessThanOrEqualsPredicate(param, range.upperEndpoint().toString())); } if (rangePredicates.size() == 1) { return rangePredicates.get(0); } if (rangePredicates.size() > 1) { return new ConjunctionPredicate(rangePredicates); } return null; } else { if (WILDCARD.equals(value)) { return new IsNotNullPredicate(param); } else { // defaults to an equals predicate with the original value return new EqualsPredicate(param, value); } } } }