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}