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 com.fasterxml.jackson.annotation.JsonIgnore;
017
018import com.fasterxml.jackson.annotation.JsonProperty;
019
020import org.gbif.api.annotation.Experimental;
021import org.gbif.api.model.common.search.SearchParameter;
022import org.gbif.api.model.occurrence.search.OccurrenceSearchParameter;
023import org.gbif.api.util.IsoDateInterval;
024import org.gbif.api.util.SearchTypeValidator;
025
026import java.util.Date;
027import java.util.Objects;
028import java.util.Optional;
029import java.util.StringJoiner;
030
031import jakarta.annotation.Nullable;
032import jakarta.validation.constraints.NotNull;
033
034import io.swagger.v3.oas.annotations.media.Schema;
035
036import static org.gbif.api.util.PreconditionUtils.checkArgument;
037
038public class SimplePredicate<S extends SearchParameter> implements Predicate {
039
040  @Schema(
041    description = "The search parameter to test.",
042    implementation = SearchParameter.class
043  )
044  @NotNull
045  private final S key;
046
047  @Schema(
048    description = "The value to test for."
049  )
050  @NotNull
051  private final String value;
052
053  @Schema(
054    description = "Whether to match letter case (UPPER or lower case) on string value comparisons."
055  )
056  @Experimental
057  @Nullable
058  private final Boolean matchCase;
059
060  protected SimplePredicate(boolean checkForNonEquals, S key, String value, Boolean matchCase) {
061    this.matchCase = matchCase;
062    Objects.requireNonNull(key, "<key> may not be null");
063    Objects.requireNonNull(value, "<value> may not be null");
064    checkArgument(!value.isEmpty(), "<value> may not be empty");
065    // make sure the value is of the right type according to the key given
066    SearchTypeValidator.validate(key, value);
067
068    this.key = key;
069    this.value = value;
070
071    checkPredicateAllowed();
072    if (checkForNonEquals) {
073      checkNonEqualsComparatorAllowed();
074    }
075  }
076
077  public S getKey() {
078    return key;
079  }
080
081  public String getValue() {
082    return value;
083  }
084
085  /**
086   * This flag enables the use of case-sensitive matches and aggregations on certain search parameters.
087   * <p>
088   * Fields that support this feature are: occurrenceId, recordedBy, samplingProtocol, catalogNumber, collectionCode,
089   * institutionCode, eventId, parentEventId, waterBody, stateProvince, recordNumber, identifiedBy, organismId and locality.
090   * <p>
091   * This is an experimental feature and its implementation map change or be removed at any time.
092   */
093  @Experimental
094  @JsonIgnore
095  public Boolean isMatchCase() {
096    return Optional.ofNullable(matchCase).orElse(Boolean.FALSE);
097  }
098
099  @JsonProperty("matchCase")
100  public Boolean getMatchCase() {
101    return matchCase;
102  }
103
104  /**
105   * @throws IllegalArgumentException if the key SearchParameter is Geometry
106   */
107  private void checkPredicateAllowed() {
108    if (OccurrenceSearchParameter.GEOMETRY == key) {
109      throw new IllegalArgumentException("Geometry parameter must use a Within predicate");
110    }
111  }
112
113  /**
114   * @throws IllegalArgumentException if the key SearchParameter allows other comparators than
115   *                                  equals
116   */
117  private void checkNonEqualsComparatorAllowed() {
118    if (!(Number.class.isAssignableFrom(key.type())
119        || Date.class.isAssignableFrom(key.type())
120        || IsoDateInterval.class.isAssignableFrom(key.type())
121        || OccurrenceSearchParameter.GEOLOGICAL_TIME == key)) {
122      throw new IllegalArgumentException(
123          "Only equals comparisons are allowed for search parameter "
124              + key
125              + " of type "
126              + key.type());
127    }
128  }
129
130  @Override
131  public boolean equals(Object o) {
132    if (this == o) {
133      return true;
134    }
135    if (o == null || getClass() != o.getClass()) {
136      return false;
137    }
138    SimplePredicate<S> that = (SimplePredicate<S>) o;
139    return key == that.key && Objects.equals(value, that.value) && matchCase == that.matchCase;
140  }
141
142  @Override
143  public int hashCode() {
144    return Objects.hash(key, value, matchCase);
145  }
146
147  @Override
148  public String toString() {
149    return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
150      .add("key=" + key)
151      .add("value='" + value + "'")
152      .add("matchCase='" + matchCase + "'")
153      .toString();
154  }
155}