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    implementation = OccurrenceSearchParameter.class
039  )
040  @NotNull
041  private final S key;
042
043  @Schema(
044    description = "The value to test for."
045  )
046  @NotNull
047  private final String value;
048
049  @Schema(
050    description = "Whether to match letter case (UPPER or lower case) on string value comparisons."
051  )
052  @Experimental
053  @Nullable
054  private final Boolean matchCase;
055
056  protected SimplePredicate(boolean checkForNonEquals, S key, String value, Boolean matchCase) {
057    this.matchCase = matchCase;
058    Objects.requireNonNull(key, "<key> may not be null");
059    Objects.requireNonNull(value, "<value> may not be null");
060    checkArgument(!value.isEmpty(), "<value> may not be empty");
061    // make sure the value is of the right type according to the key given
062    SearchTypeValidator.validate(key, value);
063
064    this.key = key;
065    this.value = value;
066
067    checkPredicateAllowed();
068    if (checkForNonEquals) {
069      checkNonEqualsComparatorAllowed();
070    }
071  }
072
073  public S getKey() {
074    return key;
075  }
076
077  public String getValue() {
078    return value;
079  }
080
081  /**
082   * This flag enables the use of case-sensitive matches and aggregations on certain search parameters.
083   * <p>
084   * Fields that support this feature are: occurrenceId, recordedBy, samplingProtocol, catalogNumber, collectionCode,
085   * institutionCode, eventId, parentEventId, waterBody, stateProvince, recordNumber, identifiedBy, organismId and locality.
086   * <p>
087   * This is an experimental feature and its implementation map change or be removed at any time.
088   */
089  @Experimental
090  public Boolean isMatchCase() {
091    return Optional.ofNullable(matchCase).orElse(Boolean.FALSE);
092  }
093
094  /**
095   * @throws IllegalArgumentException if the key SearchParameter is Geometry
096   */
097  private void checkPredicateAllowed() {
098    if (OccurrenceSearchParameter.GEOMETRY == key) {
099      throw new IllegalArgumentException("Geometry parameter must use a Within predicate");
100    }
101  }
102
103  /**
104   * @throws IllegalArgumentException if the key SearchParameter allows other comparators than
105   *                                  equals
106   */
107  private void checkNonEqualsComparatorAllowed() {
108    if (!(Number.class.isAssignableFrom(key.type()) || Date.class.isAssignableFrom(key.type()) || IsoDateInterval.class.isAssignableFrom(key.type()))) {
109      throw new IllegalArgumentException(
110        "Only equals comparisons are allowed for search parameter " + key + " of type " + key.type());
111    }
112  }
113
114  @Override
115  public boolean equals(Object o) {
116    if (this == o) {
117      return true;
118    }
119    if (o == null || getClass() != o.getClass()) {
120      return false;
121    }
122    SimplePredicate<S> that = (SimplePredicate<S>) o;
123    return key == that.key && Objects.equals(value, that.value) && matchCase == that.matchCase;
124  }
125
126  @Override
127  public int hashCode() {
128    return Objects.hash(key, value, matchCase);
129  }
130
131  @Override
132  public String toString() {
133    return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
134      .add("key=" + key)
135      .add("value='" + value + "'")
136      .add("matchCase='" + matchCase + "'")
137      .toString();
138  }
139}