001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.api.model.predicate;
015
016import org.gbif.api.annotation.Experimental;
017import org.gbif.api.model.common.search.SearchParameter;
018import org.gbif.api.model.occurrence.search.OccurrenceSearchParameter;
019import org.gbif.api.util.IsoDateInterval;
020import org.gbif.api.util.SearchTypeValidator;
021
022import java.util.Date;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.StringJoiner;
026
027import javax.annotation.Nullable;
028import javax.validation.constraints.NotNull;
029
030import io.swagger.v3.oas.annotations.media.Schema;
031
032import static org.gbif.api.util.PreconditionUtils.checkArgument;
033
034public class SimplePredicate<S extends SearchParameter> implements Predicate {
035
036  @Schema(
037    description = "The search parameter to test."
038  )
039  @NotNull
040  private final S key;
041
042  @Schema(
043    description = "The value to test for."
044  )
045  @NotNull
046  private final String value;
047
048  @Schema(
049    description = "Whether to match letter case (UPPER or lower case) on string value comparisons."
050  )
051  @Experimental
052  @Nullable
053  private final Boolean matchCase;
054
055  protected SimplePredicate(boolean checkForNonEquals, S key, String value, Boolean matchCase) {
056    this.matchCase = matchCase;
057    Objects.requireNonNull(key, "<key> may not be null");
058    Objects.requireNonNull(value, "<value> may not be null");
059    checkArgument(!value.isEmpty(), "<value> may not be empty");
060    // make sure the value is of the right type according to the key given
061    SearchTypeValidator.validate(key, value);
062
063    this.key = key;
064    this.value = value;
065
066    checkPredicateAllowed();
067    if (checkForNonEquals) {
068      checkNonEqualsComparatorAllowed();
069    }
070  }
071
072  public S getKey() {
073    return key;
074  }
075
076  public String getValue() {
077    return value;
078  }
079
080  /**
081   * This flag enables the use of case-sensitive matches and aggregations on certain search parameters.
082   * <p>
083   * Fields that support this feature are: occurrenceId, recordedBy, samplingProtocol, catalogNumber, collectionCode,
084   * institutionCode, eventId, parentEventId, waterBody, stateProvince, recordNumber, identifiedBy, organismId and locality.
085   * <p>
086   * This is an experimental feature and its implementation map change or be removed at any time.
087   */
088  @Experimental
089  public Boolean isMatchCase() {
090    return Optional.ofNullable(matchCase).orElse(Boolean.FALSE);
091  }
092
093  /**
094   * @throws IllegalArgumentException if the key SearchParameter is Geometry
095   */
096  private void checkPredicateAllowed() {
097    if (OccurrenceSearchParameter.GEOMETRY == key) {
098      throw new IllegalArgumentException("Geometry parameter must use a Within predicate");
099    }
100  }
101
102  /**
103   * @throws IllegalArgumentException if the key SearchParameter allows other comparators than
104   *                                  equals
105   */
106  private void checkNonEqualsComparatorAllowed() {
107    if (!(Number.class.isAssignableFrom(key.type()) || Date.class.isAssignableFrom(key.type()) || IsoDateInterval.class.isAssignableFrom(key.type()))) {
108      throw new IllegalArgumentException(
109        "Only equals comparisons are allowed for search parameter " + key + " of type " + key.type());
110    }
111  }
112
113  @Override
114  public boolean equals(Object o) {
115    if (this == o) {
116      return true;
117    }
118    if (o == null || getClass() != o.getClass()) {
119      return false;
120    }
121    SimplePredicate<S> that = (SimplePredicate<S>) o;
122    return key == that.key && Objects.equals(value, that.value) && matchCase == that.matchCase;
123  }
124
125  @Override
126  public int hashCode() {
127    return Objects.hash(key, value);
128  }
129
130  @Override
131  public String toString() {
132    return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
133      .add("key=" + key)
134      .add("value='" + value + "'")
135      .add("matchCase='" + matchCase + "'")
136      .toString();
137  }
138}