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.occurrence;
015
016import org.gbif.api.vocabulary.Country;
017import org.gbif.api.vocabulary.EndpointType;
018import org.gbif.api.vocabulary.GbifRegion;
019import org.gbif.dwc.terms.Term;
020import org.gbif.dwc.terms.TermFactory;
021
022import java.util.Date;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.StringJoiner;
029import java.util.UUID;
030
031import com.fasterxml.jackson.annotation.JsonProperty;
032
033import jakarta.annotation.Nullable;
034import jakarta.validation.constraints.NotNull;
035
036import org.apache.commons.lang3.StringUtils;
037
038import com.fasterxml.jackson.annotation.JsonAnyGetter;
039import com.fasterxml.jackson.annotation.JsonAnySetter;
040import com.fasterxml.jackson.annotation.JsonIgnore;
041
042import io.swagger.v3.oas.annotations.media.Schema;
043
044/**
045 * An extended map holding all core terms of an occurrence record.
046 * Major extensions that we index are also supported, i.e. media, identifiers and measurements or facts.
047 */
048@SuppressWarnings("unused")
049public class VerbatimOccurrence {
050
051  @Schema(
052    description = "Unique GBIF key for the occurrence.\n\n" +
053      "We aim to keep these keys stable, but this is not possible in every case."
054  )
055  private Long key;
056
057  @Schema(
058    description = "The UUID of the GBIF dataset containing this occurrence."
059  )
060  private UUID datasetKey;
061
062  @Schema(
063    description = "The UUID of the organization which publishes the dataset containing this occurrence."
064  )
065  private UUID publishingOrgKey;
066
067  @Schema(
068    description = "Any networks to which the dataset containing this occurrence is registered."
069  )
070  private List<UUID> networkKeys;
071
072  @Schema(
073    description = "Categories assigned to the dataset containing this occurrence."
074  )
075  private List<String> datasetCategory;
076
077  @Schema(
078    description = "The UUID of the technical installation hosted the dataset containing this occurrence."
079  )
080  private UUID installationKey;
081
082  @Schema(
083    description = "The UUID of the publishing organization which operates the technical installation hosting the " +
084      "dataset containing this occurrence."
085  )
086  private UUID hostingOrganizationKey;
087
088  @Schema(
089    description = "The country, territory or island based on ISO-3166 of the organization publishing the dataset " +
090      "containing this occurrence."
091  )
092  private Country publishingCountry;
093
094  @Schema(
095    description = "The technical protocol by which this occurrence was retrieved from the publisher's systems."
096  )
097  private EndpointType protocol;
098
099  @Schema(
100    description = "The time this occurrence was last retrieved from the publisher's systems."
101  )
102  private Date lastCrawled;
103
104  @Schema(
105    description = "The time this occurrence was last processed by GBIF's interpretation system “Pipelines”.\n\n" +
106      "This is the time the record was last changed in GBIF, **not** the time the record was last changed by the " +
107      "publisher.  Data is also reprocessed when we changed the taxonomic backbone, geographic data sources or " +
108      "other interpretation procedures.\n\n" +
109      "An earlier interpretation system distinguished between “parsing” and “interpretation”, but in the current " +
110      "system there is only one process — the two dates will always be the same."
111  )
112  private Date lastParsed;
113
114  @Schema(
115    description = "The sequence number of the attempt by GBIF to download (”crawl”), interpret and index the dataset " +
116      "to which this occurrence belongs."
117  )
118  private Integer crawlId;
119
120  /** GBIF Participation: Programme and Project */
121  @Schema(
122    description = "The identifier for a project, often assigned by a funded programme."
123  )
124  private String projectId;
125
126  @Schema(
127    description = "The identifier for a programme which funded the digitization of this occurrence."
128  )
129  private String programmeAcronym;
130
131  // the verbatim fields for the occurrence
132  @Schema(
133    description = "The verbatim fields for the occurrence, with Darwin Core terms as keys."
134  )
135  private Map<Term, String> verbatimFields = new HashMap<>();
136
137  // verbatim extension data
138  @Schema(
139    description = "The verbatim Darwin Core Archive extension fields for this occurrence.\n\n" +
140      "The main key is the record class term (the row type in Darwin Core Archive), within that are " +
141      " values with extension terms as keys."
142  )
143  private Map<String, List<Map<Term, String>>> extensions = new HashMap<>();
144
145  /**
146   * Get the value of a specific field (Term).
147   */
148  @Nullable
149  public String getVerbatimField(Term term) {
150    Objects.requireNonNull(term, "term can't be null");
151    return verbatimFields.get(term);
152  }
153
154  /**
155   * @return true if a verbatim field exists and is not null or an empty string
156   */
157  public boolean hasVerbatimField(Term term) {
158    Objects.requireNonNull(term, "term can't be null");
159    return StringUtils.isNotEmpty(verbatimFields.get(term));
160  }
161
162  /**
163   * For setting a specific field without having to replace the entire verbatimFields Map.
164   *
165   * @param term       the field to set
166   * @param fieldValue the field's value
167   */
168  public void setVerbatimField(Term term, @Nullable String fieldValue) {
169    Objects.requireNonNull(term, "term can't be null");
170    verbatimFields.put(term, fieldValue);
171  }
172
173  /**
174   * The GBIF assigned, persistent key to the occurrence record.
175   * OccurrenceID itself is kept in the verbatim verbatimFields map.
176   */
177  @NotNull
178  public Long getKey() {
179    return key;
180  }
181
182  public void setKey(Long key) {
183    this.key = key;
184  }
185
186  @NotNull
187  public UUID getDatasetKey() {
188    return datasetKey;
189  }
190
191  public void setDatasetKey(UUID datasetKey) {
192    this.datasetKey = datasetKey;
193  }
194
195  @NotNull
196  public UUID getPublishingOrgKey() {
197    return publishingOrgKey;
198  }
199
200  public void setPublishingOrgKey(UUID publishingOrgKey) {
201    this.publishingOrgKey = publishingOrgKey;
202  }
203
204  /**
205   * The GBIF Network associated to the publishing dataset.
206   */
207  @Nullable
208  public List<UUID> getNetworkKeys() {
209    return networkKeys;
210  }
211
212  public void setNetworkKeys(List<UUID> networkKeys) {
213    this.networkKeys = networkKeys;
214  }
215
216  /**
217   * Categories assigned to the dataset containing this occurrence.
218   */
219  @Nullable
220  public List<String> getDatasetCategory() {
221    return datasetCategory;
222  }
223
224  public void setDatasetCategory(List<String> datasetCategory) {
225    this.datasetCategory = datasetCategory;
226  }
227
228  /**
229   * Technical installation that publishes this occurrence record.
230   */
231  @Nullable
232  public UUID getInstallationKey() {
233    return installationKey;
234  }
235
236  public void setInstallationKey(UUID installationKey) {
237    this.installationKey = installationKey;
238  }
239
240  /**
241   * The country of the organization that publishes the dataset to which the occurrence belongs.
242   */
243  @Nullable
244  public Country getPublishingCountry() {
245    return publishingCountry;
246  }
247
248  public void setPublishingCountry(Country publishingCountry) {
249    this.publishingCountry = publishingCountry;
250  }
251
252  @Nullable
253  @JsonProperty("publishedByGbifRegion")
254  public GbifRegion getPublishedByGbifRegion() {
255    return Optional.ofNullable(publishingCountry).map(Country::getGbifRegion).orElse(null);
256  }
257
258  public void setPublishedByGbifRegion(String gbifRegion) {
259    // ignore, setter only to avoid JSON being written into the fields map
260  }
261
262  @NotNull
263  public EndpointType getProtocol() {
264    return protocol;
265  }
266
267  public void setProtocol(EndpointType protocol) {
268    this.protocol = protocol;
269  }
270
271  /**
272   * The date this record was last crawled/harvested from the endpoint.
273   */
274  @Nullable
275  public Date getLastCrawled() {
276    return lastCrawled == null ? null : new Date(lastCrawled.getTime());
277  }
278
279  public void setLastCrawled(@Nullable Date lastCrawled) {
280    this.lastCrawled = lastCrawled == null ? null : new Date(lastCrawled.getTime());
281  }
282
283  /**
284   * The date this record was last parsed from raw xml/json into verbatim verbatimFields.
285   */
286  @Nullable
287  public Date getLastParsed() {
288    return lastParsed;
289  }
290
291  public void setLastParsed(@Nullable Date lastParsed) {
292    this.lastParsed = lastParsed == null ? null : new Date(lastParsed.getTime());
293  }
294
295  /**
296   * Crawling attempt id.
297   */
298  @Nullable
299  public Integer getCrawlId() {
300    return crawlId;
301  }
302
303  public void setCrawlId(Integer crawlId) {
304    this.crawlId = crawlId;
305  }
306
307  /**
308   * GBIF project identifier.
309   */
310  @Nullable
311  public String getProjectId() {
312    return projectId;
313  }
314
315  public void setProjectId(String projectId) {
316    this.projectId = projectId;
317  }
318
319  /**
320   * GBIF programme acronym/identifier.
321   */
322  @Nullable
323  public String getProgrammeAcronym() {
324    return programmeAcronym;
325  }
326
327  public void setProgrammeAcronym(String programmeAcronym) {
328    this.programmeAcronym = programmeAcronym;
329  }
330
331  /**
332   * Organization key of the installation that hosts the occurrence record.
333   */
334  @Nullable
335  public UUID getHostingOrganizationKey() {
336    return hostingOrganizationKey;
337  }
338
339  public void setHostingOrganizationKey(UUID hostingOrganizationKey) {
340    this.hostingOrganizationKey = hostingOrganizationKey;
341  }
342
343  /**
344   * A map holding all verbatim core terms.
345   */
346  @NotNull
347  @JsonIgnore
348  public Map<Term, String> getVerbatimFields() {
349    return verbatimFields;
350  }
351
352  public void setVerbatimFields(Map<Term, String> verbatimFields) {
353    this.verbatimFields = verbatimFields;
354  }
355
356  /**
357   * A map holding all verbatim extension terms.
358   */
359  @NotNull
360  public Map<String, List<Map<Term, String>>> getExtensions() {
361    return extensions;
362  }
363
364  public void setExtensions(Map<String, List<Map<Term, String>>> extensions) {
365    this.extensions = extensions;
366  }
367
368  @Override
369  public boolean equals(Object o) {
370    if (this == o) {
371      return true;
372    }
373    if (o == null || getClass() != o.getClass()) {
374      return false;
375    }
376    VerbatimOccurrence that = (VerbatimOccurrence) o;
377    return Objects.equals(key, that.key) &&
378      Objects.equals(datasetKey, that.datasetKey) &&
379      Objects.equals(publishingOrgKey, that.publishingOrgKey) &&
380      Objects.equals(networkKeys, that.networkKeys) &&
381      Objects.equals(datasetCategory, that.datasetCategory) &&
382      Objects.equals(installationKey, that.installationKey) &&
383      publishingCountry == that.publishingCountry &&
384      protocol == that.protocol &&
385      Objects.equals(lastCrawled, that.lastCrawled) &&
386      Objects.equals(lastParsed, that.lastParsed) &&
387      Objects.equals(crawlId, that.crawlId) &&
388      Objects.equals(projectId, that.projectId) &&
389      Objects.equals(programmeAcronym, that.programmeAcronym) &&
390      Objects.equals(verbatimFields, that.verbatimFields) &&
391      Objects.equals(extensions, that.extensions);
392  }
393
394  @Override
395  public int hashCode() {
396    return Objects
397      .hash(key, datasetKey, publishingOrgKey, networkKeys, datasetCategory, installationKey, publishingCountry,
398        protocol, lastCrawled, lastParsed, crawlId, projectId, programmeAcronym, verbatimFields,
399        extensions);
400  }
401
402  @Override
403  public String toString() {
404    return new StringJoiner(", ", VerbatimOccurrence.class.getSimpleName() + "[", "]")
405      .add("key=" + key)
406      .add("datasetKey=" + datasetKey)
407      .add("publishingOrgKey=" + publishingOrgKey)
408      .add("networkKeys=" + networkKeys)
409      .add("datasetCategory=" + datasetCategory)
410      .add("installationKey=" + installationKey)
411      .add("publishingCountry=" + publishingCountry)
412      .add("protocol=" + protocol)
413      .add("lastCrawled=" + lastCrawled)
414      .add("lastParsed=" + lastParsed)
415      .add("crawlId=" + crawlId)
416      .add("projectId='" + projectId + "'")
417      .add("programmeAcronym='" + programmeAcronym + "'")
418      .add("extensions=" + extensions)
419      .toString();
420  }
421
422  /**
423   * This private method is only for deserialization via jackson and not exposed anywhere else!
424   */
425  @JsonAnySetter
426  private void addJsonVerbatimField(String key, String value) {
427    if(StringUtils.isNotEmpty(value)) {
428      Term t = TermFactory.instance().findTerm(key);
429      verbatimFields.put(t, value);
430    }
431  }
432
433  /**
434   * This private method is only for serialization via jackson and not exposed anywhere else!
435   * It maps the verbatimField terms into properties with their full qualified name.
436   */
437  @JsonAnyGetter
438  private Map<String, String> jsonVerbatimFields() { // note: for 1.6.0 MUST use non-getter name; otherwise doesn't matter
439    Map<String, String> extendedProps = new HashMap<>();
440    for (Map.Entry<Term, String> prop : verbatimFields.entrySet()) {
441      extendedProps.put(prop.getKey().qualifiedName(), prop.getValue());
442    }
443    return extendedProps;
444  }
445}