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.collections;
015
016import com.fasterxml.jackson.annotation.JsonIgnore;
017
018import io.swagger.v3.oas.annotations.Hidden;
019import io.swagger.v3.oas.annotations.media.Schema;
020
021import lombok.EqualsAndHashCode;
022import lombok.ToString;
023
024import org.gbif.api.model.common.DOI;
025import org.gbif.api.model.registry.Comment;
026import org.gbif.api.model.registry.Identifier;
027import org.gbif.api.model.registry.LenientEquals;
028import org.gbif.api.model.registry.MachineTag;
029import org.gbif.api.model.registry.PrePersist;
030import org.gbif.api.model.registry.Tag;
031import org.gbif.api.util.HttpURI;
032import org.gbif.api.util.LenientEqualsUtils;
033import org.gbif.api.util.validators.email.ValidEmail;
034import org.gbif.api.vocabulary.collections.AccessionStatus;
035import org.gbif.api.vocabulary.collections.CollectionContentType;
036import org.gbif.api.vocabulary.collections.MasterSourceType;
037import org.gbif.api.vocabulary.collections.PreservationType;
038
039import java.net.URI;
040import java.util.ArrayList;
041import java.util.Date;
042import java.util.HashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.Objects;
046import java.util.StringJoiner;
047import java.util.UUID;
048
049import javax.annotation.Nullable;
050import javax.validation.Valid;
051import javax.validation.constraints.NotNull;
052import javax.validation.constraints.Size;
053
054/**
055 * A group of specimens or other natural history objects. Types of collections can be: specimens,
056 * original artwork, archives, observations, library materials, datasets, photographs or mixed
057 * collections such as those that result from expeditions and voyages of discovery.
058 */
059@SuppressWarnings("unused")
060@ToString
061@EqualsAndHashCode
062public class Collection implements CollectionEntity, LenientEquals<Collection> {
063
064  @Schema(
065      description = "Unique GBIF key for the collection.",
066      accessMode = Schema.AccessMode.READ_ONLY)
067  private UUID key;
068
069  @Schema(
070      description =
071          "Code of the collection — identifies a collection at the "
072              + "owner's location.\n\n"
073              + "*(NB Not required for updates.)*")
074  @Sourceable(masterSources = MasterSourceType.IH)
075  private String code;
076
077  @Schema(
078      description = "Descriptive name of the collection.\n\n" + "*(NB Not required for updates.)*")
079  @Sourceable(masterSources = MasterSourceType.GBIF_REGISTRY)
080  @Sourceable(masterSources = MasterSourceType.IH, overridable = true)
081  private String name;
082
083  @Schema(description = "Description or summary of the contents of the collection.")
084  @Sourceable(masterSources = MasterSourceType.GBIF_REGISTRY)
085  private String description;
086
087  @Schema(description = "Content type of the elements found in the collection.")
088  private List<CollectionContentType> contentTypes = new ArrayList<>();
089
090  @Schema(description = "Whether the collection is active/maintained.")
091  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
092  private boolean active;
093
094  @Schema(description = "Whether this collection belongs to an individual.")
095  private boolean personalCollection;
096
097  @Schema(
098      description = "A Digital Object Identifier for the collection.",
099      implementation = String.class,
100      pattern = "(10(?:\\.[0-9]+)+)" + "/(.+)")
101  private DOI doi;
102
103  @Schema(description = "Email addresses associated with the collection.")
104  @Sourceable(masterSources = MasterSourceType.IH)
105  private List<@ValidEmail String> email = new ArrayList<>();
106
107  @Schema(description = "Telephone numbers associated with the collection.")
108  @Sourceable(masterSources = MasterSourceType.IH)
109  private List<String> phone = new ArrayList<>();
110
111  @Schema(description = "The collection's WWW homepage.")
112  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
113  private URI homepage;
114
115  @Schema(description = "A URL for an interactive catalogue of the collection.")
116  private URI catalogUrl;
117
118  @Schema(description = "A URL for a machine-readable API for the collection catalogue.")
119  private URI apiUrl;
120
121  @Schema(description = "The preservation mechanisms used for this collection.")
122  @Sourceable(masterSources = MasterSourceType.GBIF_REGISTRY)
123  private List<PreservationType> preservationTypes = new ArrayList<>();
124
125  @Schema(description = "How the collection was added or joined.")
126  private AccessionStatus accessionStatus;
127
128  @Schema(description = "The key of the institution owning or hosting the collection.")
129  private UUID institutionKey;
130
131  @Schema(description = "The postal address of the collection.")
132  @Sourceable(masterSources = MasterSourceType.IH)
133  private Address mailingAddress;
134
135  @Schema(description = "The address of the location of the collection.")
136  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
137  private Address address;
138
139  @Schema(
140      description =
141          "The GBIF username of the creator of the collection entity in " + "the GBIF registry.",
142      accessMode = Schema.AccessMode.READ_ONLY)
143  private String createdBy;
144
145  @Schema(
146      description =
147          "The GBIF username of the last user to modify the collection "
148              + "entity in the GBIF registry.",
149      accessMode = Schema.AccessMode.READ_ONLY)
150  private String modifiedBy;
151
152  @Schema(
153      description =
154          "Timestamp of when the collection entity was created in the GBIF " + "registry.",
155      accessMode = Schema.AccessMode.READ_ONLY)
156  private Date created;
157
158  @Schema(
159      description =
160          "Timestamp of when the collection entity was modified in the GBIF " + "registry.",
161      accessMode = Schema.AccessMode.READ_ONLY)
162  private Date modified;
163
164  @Schema(
165      description =
166          "If present, the collection was deleted at this time. "
167              + "It is possible for it to be restored in the future.",
168      accessMode = Schema.AccessMode.READ_ONLY)
169  private Date deleted;
170
171  @Schema(
172      description = "A list of tags associated with this collection.",
173      accessMode = Schema.AccessMode.READ_ONLY)
174  private List<Tag> tags = new ArrayList<>();
175
176  @Schema(
177      description = "A list of identifiers associated with this collection.",
178      accessMode = Schema.AccessMode.READ_ONLY)
179  @Sourceable(masterSources = MasterSourceType.IH, sourceableParts = "IH_IRN")
180  @Sourceable(masterSources = MasterSourceType.GBIF_REGISTRY, sourceableParts = "DOI")
181  private List<Identifier> identifiers = new ArrayList<>();
182
183  @Schema(
184      description = "A list of contact people for this collection.",
185      accessMode = Schema.AccessMode.READ_ONLY)
186  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
187  private List<Contact> contactPersons = new ArrayList<>();
188
189  @Schema(description = "Whether there is a record for this collection in *Index Herbariorum*.")
190  @Sourceable(masterSources = MasterSourceType.IH)
191  private boolean indexHerbariorumRecord;
192
193  @Schema(description = "The number of specimens contained in this collection.")
194  @Sourceable(masterSources = MasterSourceType.IH)
195  private Integer numberSpecimens;
196
197  @Schema(
198      description = "A list of machine tags associated with this collection.",
199      accessMode = Schema.AccessMode.READ_ONLY)
200  private List<MachineTag> machineTags = new ArrayList<>();
201
202  @Schema(description = "The taxonomic coverage of this collection.")
203  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
204  private String taxonomicCoverage;
205
206  @Schema(description = "The geographic scope of this collection.")
207  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
208  private String geography;
209
210  @Schema(description = "Notes on the collection.")
211  @Sourceable(masterSources = MasterSourceType.IH)
212  private String notes;
213
214  @Schema(description = "Other collections incorporated into this collection.")
215  @Sourceable(masterSources = {MasterSourceType.GBIF_REGISTRY, MasterSourceType.IH})
216  private List<String> incorporatedCollections = new ArrayList<>();
217
218  @Schema(description = "Important collectors who have specimens in this collection.")
219  @Sourceable(masterSources = MasterSourceType.IH)
220  private List<String> importantCollectors = new ArrayList<>();
221
222  @Schema(
223      description = "A summary of the collection." // TODO, a summary of what?
224      )
225  @Sourceable(masterSources = MasterSourceType.IH)
226  private Map<String, Integer> collectionSummary = new HashMap<>();
227
228  @Schema(description = "Alternative codes for this collection.")
229  private List<AlternativeCode> alternativeCodes = new ArrayList<>();
230
231  @Schema(
232      description = "A list of comments associated with this collection.",
233      accessMode = Schema.AccessMode.READ_ONLY)
234  private List<Comment> comments = new ArrayList<>();
235
236  @Schema(
237      description = "Mapping of a GRSciColl collection to occurrence records",
238      accessMode = Schema.AccessMode.READ_ONLY)
239  private List<OccurrenceMapping> occurrenceMappings = new ArrayList<>();
240
241  @Schema(description = "A collection record that replaces this collection.")
242  private UUID replacedBy;
243
244  @Schema(description = "The primary source of this collection record.")
245  private MasterSourceType masterSource;
246
247  @Schema(
248      description =
249          "Information to assist the synchronization of the master source "
250              + "record with the record in the GBIF registry.")
251  private MasterSourceMetadata masterSourceMetadata;
252
253  @Schema(description = "An organizational department managing the collection.")
254  @Sourceable(masterSources = MasterSourceType.IH)
255  private String department;
256
257  @Schema(description = "An organizational division managing the collection.")
258  @Sourceable(masterSources = MasterSourceType.IH)
259  private String division;
260
261  @Schema(description = "Whether the collection is shown on the NHC portal.")
262  private Boolean displayOnNHCPortal;
263
264  @Schema(description = "An estimate of the number of occurrences linked to the institution.")
265  private Integer occurrenceCount;
266
267  @Schema(description = "An estimate of the number of type specimens linked to the institution.")
268  private Integer typeSpecimenCount;
269
270  /** List of alternative identifiers: UUIDs, external system identifiers, LSIDs, etc.. */
271  @Override
272  public List<Identifier> getIdentifiers() {
273    return identifiers;
274  }
275
276  @Override
277  public void setIdentifiers(List<Identifier> identifiers) {
278    this.identifiers = identifiers;
279  }
280
281  /** (Meta)Tags or labels. */
282  @Valid
283  @Override
284  public List<Tag> getTags() {
285    return tags;
286  }
287
288  @Override
289  public void setTags(List<Tag> tags) {
290    this.tags = tags;
291  }
292
293  /** GBIF Unique identifier of this collection. */
294  @Override
295  public UUID getKey() {
296    return key;
297  }
298
299  @Override
300  public void setKey(UUID key) {
301    this.key = key;
302  }
303
304  /** Collection code: identifies a collection at the owner's location. */
305  @NotNull(groups = PrePersist.class)
306  @Override
307  public String getCode() {
308    return code;
309  }
310
311  @Override
312  public void setCode(String code) {
313    this.code = code;
314  }
315
316  /** Descriptive name of a collection. */
317  @NotNull
318  @Override
319  public String getName() {
320    return name;
321  }
322
323  @Override
324  public void setName(String name) {
325    this.name = name;
326  }
327
328  /** Textual description/summary of the contents of a collection. */
329  @Size(min = 1)
330  @Override
331  public String getDescription() {
332    return description;
333  }
334
335  @Override
336  public void setDescription(String description) {
337    this.description = description;
338  }
339
340  /** Content type of the elements found in a collection. */
341  public List<CollectionContentType> getContentTypes() {
342    return contentTypes;
343  }
344
345  public void setContentTypes(List<CollectionContentType> contentTypes) {
346    this.contentTypes = contentTypes;
347  }
348
349  /** Is this collection currently active/maintained. */
350  @Override
351  public boolean isActive() {
352    return active;
353  }
354
355  @Override
356  public void setActive(boolean active) {
357    this.active = active;
358  }
359
360  /** Does this collections belong to an individual?. */
361  public boolean isPersonalCollection() {
362    return personalCollection;
363  }
364
365  public void setPersonalCollection(boolean personalCollection) {
366    this.personalCollection = personalCollection;
367  }
368
369  /** Digital Object Identifier assigned to this collection. */
370  public DOI getDoi() {
371    return doi;
372  }
373
374  public void setDoi(DOI doi) {
375    this.doi = doi;
376  }
377
378  @Override
379  public List<String> getEmail() {
380    return email;
381  }
382
383  @Override
384  public void setEmail(List<String> email) {
385    this.email = email;
386  }
387
388  @Override
389  public List<String> getPhone() {
390    return phone;
391  }
392
393  @Override
394  public void setPhone(List<String> phone) {
395    this.phone = phone;
396  }
397
398  /** URL containing information about a collection. */
399  @HttpURI
400  @Nullable
401  public URI getHomepage() {
402    return homepage;
403  }
404
405  public void setHomepage(URI homepage) {
406    this.homepage = homepage;
407  }
408
409  /** URI that exposes data about the catalog associated to a collection. */
410  @HttpURI
411  @Nullable
412  public URI getCatalogUrl() {
413    return catalogUrl;
414  }
415
416  public void setCatalogUrl(URI catalogUrl) {
417    this.catalogUrl = catalogUrl;
418  }
419
420  /** Machine consumable endpoint of information about a collection. */
421  @HttpURI
422  @Nullable
423  public URI getApiUrl() {
424    return apiUrl;
425  }
426
427  public void setApiUrl(URI apiUrl) {
428    this.apiUrl = apiUrl;
429  }
430
431  /** Types of preservation mechanisms used for this collections. */
432  public List<PreservationType> getPreservationTypes() {
433    return preservationTypes;
434  }
435
436  public void setPreservationTypes(List<PreservationType> preservationTypes) {
437    this.preservationTypes = preservationTypes;
438  }
439
440  /** Defines how a collection as been added or joined. */
441  public AccessionStatus getAccessionStatus() {
442    return accessionStatus;
443  }
444
445  public void setAccessionStatus(AccessionStatus accessionStatus) {
446    this.accessionStatus = accessionStatus;
447  }
448
449  /** Institution that owns or hosts this collection. */
450  public UUID getInstitutionKey() {
451    return institutionKey;
452  }
453
454  public void setInstitutionKey(UUID institutionKey) {
455    this.institutionKey = institutionKey;
456  }
457
458  /** Address used to send/receive physical mail. */
459  @Nullable
460  @Valid
461  @Override
462  public Address getMailingAddress() {
463    return mailingAddress;
464  }
465
466  @Override
467  public void setMailingAddress(Address mailingAddress) {
468    this.mailingAddress = mailingAddress;
469  }
470
471  /** Address where this collection is situated. */
472  @Nullable
473  @Valid
474  @Override
475  public Address getAddress() {
476    return address;
477  }
478
479  @Override
480  public void setAddress(Address address) {
481    this.address = address;
482  }
483
484  @Override
485  public String getCreatedBy() {
486    return createdBy;
487  }
488
489  @Override
490  public void setCreatedBy(String createdBy) {
491    this.createdBy = createdBy;
492  }
493
494  @Override
495  public String getModifiedBy() {
496    return modifiedBy;
497  }
498
499  @Override
500  public void setModifiedBy(String modifiedBy) {
501    this.modifiedBy = modifiedBy;
502  }
503
504  @Override
505  public Date getCreated() {
506    return created;
507  }
508
509  @Override
510  public void setCreated(Date created) {
511    this.created = created;
512  }
513
514  @Override
515  public Date getModified() {
516    return modified;
517  }
518
519  @Override
520  public void setModified(Date modified) {
521    this.modified = modified;
522  }
523
524  @Override
525  public Date getDeleted() {
526    return deleted;
527  }
528
529  @Override
530  public void setDeleted(Date deleted) {
531    this.deleted = deleted;
532  }
533
534  @Valid
535  @Override
536  public List<Contact> getContactPersons() {
537    return contactPersons;
538  }
539
540  @Override
541  public void setContactPersons(List<Contact> contactPersons) {
542    this.contactPersons = contactPersons;
543  }
544
545  public boolean isIndexHerbariorumRecord() {
546    return indexHerbariorumRecord;
547  }
548
549  public void setIndexHerbariorumRecord(boolean indexHerbariorumRecord) {
550    this.indexHerbariorumRecord = indexHerbariorumRecord;
551  }
552
553  public Integer getNumberSpecimens() {
554    return numberSpecimens;
555  }
556
557  public void setNumberSpecimens(Integer numberSpecimens) {
558    this.numberSpecimens = numberSpecimens;
559  }
560
561  @Valid
562  @Override
563  public @NotNull List<MachineTag> getMachineTags() {
564    return machineTags;
565  }
566
567  @Override
568  public void setMachineTags(List<MachineTag> machineTags) {
569    this.machineTags = machineTags;
570  }
571
572  @Override
573  public void addMachineTag(MachineTag machineTag) {
574    machineTags.add(machineTag);
575  }
576
577  public String getTaxonomicCoverage() {
578    return taxonomicCoverage;
579  }
580
581  public void setTaxonomicCoverage(String taxonomicCoverage) {
582    this.taxonomicCoverage = taxonomicCoverage;
583  }
584
585  public String getGeography() {
586    return geography;
587  }
588
589  public void setGeography(String geography) {
590    this.geography = geography;
591  }
592
593  public String getNotes() {
594    return notes;
595  }
596
597  public void setNotes(String notes) {
598    this.notes = notes;
599  }
600
601  public List<String> getIncorporatedCollections() {
602    return incorporatedCollections;
603  }
604
605  public void setIncorporatedCollections(List<String> incorporatedCollections) {
606    this.incorporatedCollections = incorporatedCollections;
607  }
608
609  public List<String> getImportantCollectors() {
610    return importantCollectors;
611  }
612
613  public void setImportantCollectors(List<String> importantCollectors) {
614    this.importantCollectors = importantCollectors;
615  }
616
617  public Map<String, Integer> getCollectionSummary() {
618    return collectionSummary;
619  }
620
621  public void setCollectionSummary(Map<String, Integer> collectionSummary) {
622    this.collectionSummary = collectionSummary;
623  }
624
625  /** Alternative codes for a collection. */
626  @Override
627  public List<AlternativeCode> getAlternativeCodes() {
628    return alternativeCodes;
629  }
630
631  @Override
632  public void setAlternativeCodes(List<AlternativeCode> alternativeCodes) {
633    this.alternativeCodes = alternativeCodes;
634  }
635
636  @Override
637  public @NotNull List<Comment> getComments() {
638    return comments;
639  }
640
641  @Override
642  public void setComments(List<Comment> comments) {
643    this.comments = comments;
644  }
645
646  @Override
647  public @NotNull List<OccurrenceMapping> getOccurrenceMappings() {
648    return occurrenceMappings;
649  }
650
651  @Override
652  public void setOccurrenceMappings(List<OccurrenceMapping> occurrenceMappings) {
653    this.occurrenceMappings = occurrenceMappings;
654  }
655
656  @Override
657  public UUID getReplacedBy() {
658    return replacedBy;
659  }
660
661  @Override
662  public void setReplacedBy(UUID replacedBy) {
663    this.replacedBy = replacedBy;
664  }
665
666  @Override
667  public MasterSourceType getMasterSource() {
668    return masterSource;
669  }
670
671  @Override
672  public void setMasterSource(MasterSourceType masterSource) {
673    this.masterSource = masterSource;
674  }
675
676  @Override
677  public MasterSourceMetadata getMasterSourceMetadata() {
678    return masterSourceMetadata;
679  }
680
681  @Override
682  public void setMasterSourceMetadata(MasterSourceMetadata masterSourceMetadata) {
683    this.masterSourceMetadata = masterSourceMetadata;
684  }
685
686  public String getDepartment() {
687    return department;
688  }
689
690  public void setDepartment(String department) {
691    this.department = department;
692  }
693
694  public String getDivision() {
695    return division;
696  }
697
698  public void setDivision(String division) {
699    this.division = division;
700  }
701
702  @Override
703  public Boolean getDisplayOnNHCPortal() {
704    return displayOnNHCPortal;
705  }
706
707  @Override
708  public void setDisplayOnNHCPortal(Boolean displayOnNHCPortal) {
709    this.displayOnNHCPortal = displayOnNHCPortal;
710  }
711
712  @Hidden
713  @JsonIgnore
714  public String getCountry() {
715    if (address != null && address.getCountry() != null) {
716      return address.getCountry().getIso2LetterCode();
717    } else if (mailingAddress != null && mailingAddress.getCountry() != null) {
718      return mailingAddress.getCountry().getIso2LetterCode();
719    }
720    return null;
721  }
722
723  @Hidden
724  @JsonIgnore
725  public String getCity() {
726    if (address != null && address.getCity() != null) {
727      return address.getCity();
728    } else if (mailingAddress != null && mailingAddress.getCity() != null) {
729      return mailingAddress.getCity();
730    }
731    return null;
732  }
733
734  @Hidden
735  @JsonIgnore
736  public String getProvince() {
737    if (address != null && address.getProvince() != null) {
738      return address.getProvince();
739    } else if (mailingAddress != null && mailingAddress.getProvince() != null) {
740      return mailingAddress.getProvince();
741    }
742    return null;
743  }
744
745  public Integer getOccurrenceCount() {
746    return occurrenceCount;
747  }
748
749  public void setOccurrenceCount(Integer occurrenceCount) {
750    this.occurrenceCount = occurrenceCount;
751  }
752
753  public Integer getTypeSpecimenCount() {
754    return typeSpecimenCount;
755  }
756
757  public void setTypeSpecimenCount(Integer typeSpecimenCount) {
758    this.typeSpecimenCount = typeSpecimenCount;
759  }
760
761  @Override
762  public boolean lenientEquals(Collection other) {
763    if (this == other) {
764      return true;
765    }
766    return active == other.active
767        && personalCollection == other.personalCollection
768        && Objects.equals(key, other.key)
769        && Objects.equals(code, other.code)
770        && Objects.equals(name, other.name)
771        && Objects.equals(description, other.description)
772        && Objects.equals(contentTypes, other.contentTypes)
773        && Objects.equals(doi, other.doi)
774        && Objects.equals(email, other.email)
775        && Objects.equals(phone, other.phone)
776        && Objects.equals(homepage, other.homepage)
777        && Objects.equals(catalogUrl, other.catalogUrl)
778        && Objects.equals(apiUrl, other.apiUrl)
779        && Objects.equals(preservationTypes, other.preservationTypes)
780        && accessionStatus == other.accessionStatus
781        && Objects.equals(institutionKey, other.institutionKey)
782        && LenientEqualsUtils.lenientEquals(mailingAddress, other.mailingAddress)
783        && LenientEqualsUtils.lenientEquals(address, other.address)
784        && Objects.equals(deleted, other.deleted)
785        && indexHerbariorumRecord == other.indexHerbariorumRecord
786        && Objects.equals(numberSpecimens, other.numberSpecimens)
787        && Objects.equals(taxonomicCoverage, other.taxonomicCoverage)
788        && Objects.equals(geography, other.geography)
789        && Objects.equals(notes, other.notes)
790        && Objects.equals(incorporatedCollections, other.incorporatedCollections)
791        && Objects.equals(importantCollectors, other.importantCollectors)
792        && Objects.equals(collectionSummary, other.collectionSummary)
793        && Objects.equals(alternativeCodes, other.alternativeCodes)
794        && Objects.equals(comments, other.comments)
795        && Objects.equals(occurrenceMappings, other.occurrenceMappings)
796        && Objects.equals(replacedBy, other.replacedBy)
797        && Objects.equals(masterSource, other.masterSource)
798        && Objects.equals(masterSourceMetadata, other.masterSourceMetadata)
799        && Objects.equals(division, other.division)
800        && Objects.equals(department, other.department)
801        && Objects.equals(displayOnNHCPortal, other.displayOnNHCPortal);
802  }
803}