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