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.event; 015 016import com.fasterxml.jackson.annotation.JsonAnyGetter; 017import com.fasterxml.jackson.annotation.JsonIgnore; 018import com.fasterxml.jackson.annotation.JsonProperty; 019import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 020import com.fasterxml.jackson.databind.annotation.JsonSerialize; 021import io.swagger.v3.oas.annotations.ExternalDocumentation; 022import io.swagger.v3.oas.annotations.media.Schema; 023import jakarta.annotation.Nullable; 024import java.lang.reflect.Field; 025import java.lang.reflect.Modifier; 026import java.net.URI; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.Date; 032import java.util.EnumSet; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.Objects; 037import java.util.Set; 038import java.util.stream.Collectors; 039import java.util.stream.Stream; 040import lombok.AllArgsConstructor; 041import lombok.Data; 042import org.gbif.api.model.common.Identifier; 043import org.gbif.api.model.common.MediaObject; 044import org.gbif.api.model.occurrence.Gadm; 045import org.gbif.api.model.occurrence.MeasurementOrFact; 046import org.gbif.api.model.occurrence.Occurrence; 047import org.gbif.api.model.occurrence.OccurrenceRelation; 048import org.gbif.api.model.occurrence.VerbatimOccurrence; 049import org.gbif.api.util.IsoDateInterval; 050import org.gbif.api.vocabulary.Continent; 051import org.gbif.api.vocabulary.Country; 052import org.gbif.api.vocabulary.EventIssue; 053import org.gbif.api.vocabulary.License; 054import org.gbif.api.vocabulary.OccurrenceIssue; 055import org.gbif.dwc.terms.DwcTerm; 056import org.gbif.dwc.terms.Term; 057import org.gbif.dwc.terms.UnknownTerm; 058 059/** 060 * Event class based on https://dwc.tdwg.org/terms/#event. 061 */ 062@Data 063public class Event extends VerbatimOccurrence { 064 065 public static final String GEO_DATUM = "WGS84"; 066 067 // keep names of ALL properties of this class in a set for jackson serialization, see #properties() 068 private static final Set<String> PROPERTIES = Collections.unmodifiableSet( 069 Stream.concat( 070 // we need to these json properties manually because we have a fixed getter but no field for it 071 Stream.of(DwcTerm.geodeticDatum.simpleName(), "class", "countryCode"), 072 Stream.concat(Arrays.stream(Event.class.getDeclaredFields()), 073 Arrays.stream(VerbatimOccurrence.class.getDeclaredFields())) 074 .filter(field -> !Modifier.isStatic(field.getModifiers())) 075 .map(Field::getName)).collect(Collectors.toSet())); 076 077 public static Event fromOccurrence(Occurrence occurrence) { 078 Event event =new Event(); 079 event.setKey(occurrence.getKey()); 080 event.setDatasetKey(occurrence.getDatasetKey()); 081 event.setPublishingOrgKey(occurrence.getPublishingOrgKey()); 082 event.setNetworkKeys(occurrence.getNetworkKeys()); 083 event.setDatasetCategory(occurrence.getDatasetCategory()); 084 event.setInstallationKey(occurrence.getInstallationKey()); 085 event.setPublishingCountry(occurrence.getPublishingCountry()); 086 event.setProtocol(occurrence.getProtocol()); 087 event.setLastCrawled(occurrence.getLastCrawled()); 088 event.setLastParsed(occurrence.getLastParsed()); 089 event.setCrawlId(occurrence.getCrawlId()); 090 event.setProjectId(occurrence.getProjectId()); 091 event.setProgrammeAcronym(occurrence.getProgrammeAcronym()); 092 event.setHostingOrganizationKey(occurrence.getHostingOrganizationKey()); 093 event.setVerbatimFields(occurrence.getVerbatimFields()); 094 event.setExtensions(occurrence.getExtensions()); 095 event.setDateIdentified(occurrence.getDateIdentified()); 096 event.setDecimalLongitude(occurrence.getDecimalLongitude()); 097 event.setDecimalLatitude(occurrence.getDecimalLatitude()); 098 event.setCoordinatePrecision(occurrence.getCoordinatePrecision()); 099 event.setCoordinateUncertaintyInMeters(occurrence.getCoordinateUncertaintyInMeters()); 100 event.setElevation(occurrence.getElevation()); 101 event.setElevationAccuracy(occurrence.getElevationAccuracy()); 102 event.setDepth(occurrence.getDepth()); 103 event.setDepthAccuracy(occurrence.getDepthAccuracy()); 104 event.setContinent(occurrence.getContinent()); 105 event.setCountry(occurrence.getCountry()); 106 event.setStateProvince(occurrence.getStateProvince()); 107 event.setWaterBody(occurrence.getWaterBody()); 108 event.setDistanceFromCentroidInMeters(occurrence.getDistanceFromCentroidInMeters()); 109 event.setYear(occurrence.getYear()); 110 event.setMonth(occurrence.getMonth()); 111 event.setDay(occurrence.getDay()); 112 event.setEventDate(occurrence.getEventDate()); 113 event.setIssues(toEventIssues(occurrence.getIssues())); 114 event.setModified(occurrence.getModified()); 115 event.setLastInterpreted(occurrence.getLastInterpreted()); 116 event.setReferences(occurrence.getReferences()); 117 event.setLicense(occurrence.getLicense()); 118 event.setOrganismQuantity(occurrence.getOrganismQuantity()); 119 event.setOrganismQuantityType(occurrence.getOrganismQuantityType()); 120 event.setSampleSizeUnit(occurrence.getSampleSizeUnit()); 121 event.setSampleSizeValue(occurrence.getSampleSizeValue()); 122 event.setRelativeOrganismQuantity(occurrence.getRelativeOrganismQuantity()); 123 event.setIdentifiers(occurrence.getIdentifiers()); 124 event.setMedia(occurrence.getMedia()); 125 event.setFacts(occurrence.getFacts()); 126 event.setRelations(occurrence.getRelations()); 127 event.setGadm(occurrence.getGadm()); 128 event.setDatasetID(occurrence.getDatasetID()); 129 event.setDatasetName(occurrence.getDatasetName()); 130 event.setPreparations(occurrence.getPreparations()); 131 event.setSamplingProtocol(occurrence.getSamplingProtocol()); 132 return event; 133 } 134 135 private static Set<EventIssue> toEventIssues(Collection<OccurrenceIssue> occurrenceIssues) { 136 return occurrenceIssues.stream() 137 .map( 138 occIssue -> { 139 try { 140 return EventIssue.valueOf(occIssue.name()); 141 } catch (Exception ex) { 142 // we ignore the value 143 } 144 return null; 145 }) 146 .filter(Objects::nonNull) 147 .collect(Collectors.toSet()); 148 } 149 150 @Data 151 @AllArgsConstructor 152 public static class ParentLineage { 153 private String id; 154 private String eventType; 155 } 156 157 @Data 158 @AllArgsConstructor 159 public static class VocabularyConcept { 160 private String concept; 161 private Set<String> lineage; 162 } 163 164 private String id; 165 private String eventID; 166 private String parentEventID; 167 private Integer startDayOfYear; 168 private Integer endDayOfYear; 169 private String locationID; 170 private String eventType; 171 private List<ParentLineage> parentsLineage; 172 173 // identification 174 private Date dateIdentified; 175 // location 176 private Double decimalLongitude; 177 private Double decimalLatitude; 178 179 //coordinatePrecision and coordinateUncertaintyInMeters should be BigDecimal see POR-2795 180 private Double coordinatePrecision; 181 private Double coordinateUncertaintyInMeters; 182 183 private Double elevation; 184 private Double elevationAccuracy; 185 private Double depth; 186 private Double depthAccuracy; 187 private Continent continent; 188 @JsonSerialize(using = Country.IsoSerializer.class) 189 @JsonDeserialize(using = Country.IsoDeserializer.class) 190 private Country country; 191 private String stateProvince; 192 private String waterBody; 193 private Double distanceFromCentroidInMeters; 194 195 // recording event 196 private Integer year; 197 private Integer month; 198 private Integer day; 199 private IsoDateInterval eventDate; 200 201 private Set<EventIssue> issues = EnumSet.noneOf(EventIssue.class); 202 203 // record level 204 private Date modified; // interpreted dc:modified, i.e. date changed in source 205 private Date lastInterpreted; 206 private URI references; 207 private License license; 208 private Double organismQuantity; 209 private String organismQuantityType; 210 private String sampleSizeUnit; 211 private Double sampleSizeValue; 212 private Double relativeOrganismQuantity; 213 private String projectTitle; 214 private String fundingAttribution; 215 private String fundingAttributionID; 216 217 218 // interpreted extension data 219 private List<Identifier> identifiers = new ArrayList<>(); 220 private List<MediaObject> media = new ArrayList<>(); 221 private List<MeasurementOrFact> facts = new ArrayList<>(); 222 private List<OccurrenceRelation> relations = new ArrayList<>(); 223 private Gadm gadm = new Gadm(); 224 private String datasetID; 225 private String datasetName; 226 private String preparations; 227 private String samplingProtocol; 228 private List<Humboldt> humboldt = new ArrayList<>(); 229 230 @Schema( 231 description = "The 2-letter country code (as per ISO-3166-1) of the country, territory or area in which the " + 232 "occurrence was recorded.", 233 externalDocs = @ExternalDocumentation( 234 description = "Darwin Core definition", 235 url = "https://rs.tdwg.org/dwc/terms/countryCode" 236 ) 237 ) 238 @Nullable 239 @JsonProperty("countryCode") 240 public Country getCountry() { 241 return country; 242 } 243 244 /** 245 * Renders the country title as a JSON property country in addition to the ISO 3166 2 letter countryCode being 246 * serialized by the regular country Java property. 247 * Made private to use it only for JSON serialization and not within Java code. 248 */ 249 @Schema( 250 description = "The title (as per ISO-3166-1) of the country, territory or area in which the " + 251 "occurrence was recorded.", 252 externalDocs = @ExternalDocumentation( 253 description = "Darwin Core definition", 254 url = "https://rs.tdwg.org/dwc/terms/country" 255 ) 256 ) 257 @Nullable 258 @JsonProperty("country") 259 private String getCountryTitle() { 260 return country == null ? null : country.getTitle(); 261 } 262 263 private void setCountryTitle(String country) { 264 // ignore, setter only to avoid JSON being written into the fields map 265 } 266 267 /** 268 * Convenience method checking if any spatial validation rule has not passed. 269 * Primarily used to indicate that the record should not be displayed on a map. 270 */ 271 @JsonIgnore 272 public boolean hasSpatialIssue() { 273 for (OccurrenceIssue rule : OccurrenceIssue.GEOSPATIAL_RULES) { 274 if (issues.contains(rule)) { 275 return true; 276 } 277 } 278 return false; 279 } 280 281 /** 282 * The geodetic datum for the interpreted decimal coordinates. 283 * This is always WGS84 if there a coordinate exists as we reproject other datums into WGS84. 284 */ 285 @Nullable 286 public String getGeodeticDatum() { 287 if (decimalLatitude != null) { 288 return GEO_DATUM; 289 } 290 return null; 291 } 292 293 /** 294 * This private method is only for serialization via jackson and not exposed anywhere else! 295 * It maps the verbatimField terms into properties with their simple name or qualified names for UnknownTerms. 296 */ 297 @JsonAnyGetter 298 private Map<String, String> jsonVerbatimFields() { 299 Map<String, String> extendedProps = new HashMap<>(); 300 for (Map.Entry<Term, String> prop : getVerbatimFields().entrySet()) { 301 Term t = prop.getKey(); 302 if (t instanceof UnknownTerm || PROPERTIES.contains(t.simpleName())) { 303 extendedProps.put(t.qualifiedName(), prop.getValue()); 304 } else { 305 // render all terms in controlled enumerations as simple names only - unless we have a property of that name already! 306 extendedProps.put(t.simpleName(), prop.getValue()); 307 } 308 } 309 return extendedProps; 310 } 311}