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