package org.gbif.occurrence.ws.provider; import org.gbif.api.model.occurrence.Occurrence; import org.gbif.api.vocabulary.Country; import org.gbif.api.vocabulary.OccurrenceIssue; import org.gbif.dwc.terms.DcTerm; import org.gbif.dwc.terms.DwcTerm; import org.gbif.dwc.terms.GbifTerm; import org.gbif.dwc.terms.Term; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Date; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.FastDateFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Custom {@link MessageBodyWriter} to serialize {@link Occurrence} in DarwinCore XML. * We do not use JAXB annotations to keep the distinction between the model and its XML representation. * It is also easier to manage properties like Country, List, Map. * */ @Provider @Produces(MediaType.APPLICATION_XML) public class OccurrenceDwcXMLBodyWriter implements MessageBodyWriter<Occurrence> { private static final Logger LOG = LoggerFactory.getLogger(OccurrenceDwcXMLBodyWriter.class); private static final FastDateFormat FDF = DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; /** * Transforms an {@link Occurrence} object into a byte[] representing a XML document. * * @param occurrence * @return the {@link Occurrence} as byte[] * @throws WebApplicationException if something went wrong while generating the XML document */ private byte[] occurrenceXMLAsByteArray(Occurrence occurrence) throws WebApplicationException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { DwcXMLDocument dwcXMLDocument = DwcXMLDocument.newInstance(DwcTerm.Occurrence); appendIfNotNull(dwcXMLDocument, GbifTerm.gbifID, occurrence.getKey()); //this may be not the most compact way to serialize an Occurrence (e.g. reflection) but //it gives more freedom to handle things like date and country fields appendIfNotNull(dwcXMLDocument, DwcTerm.basisOfRecord, occurrence.getBasisOfRecord()); appendIfNotNull(dwcXMLDocument, DwcTerm.individualCount, occurrence.getIndividualCount()); appendIfNotNull(dwcXMLDocument, DwcTerm.sex, occurrence.getSex()); appendIfNotNull(dwcXMLDocument, DwcTerm.lifeStage, occurrence.getLifeStage()); appendIfNotNull(dwcXMLDocument, DwcTerm.establishmentMeans, occurrence.getEstablishmentMeans()); appendIfNotNull(dwcXMLDocument, GbifTerm.taxonKey, occurrence.getTaxonKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.kingdomKey, occurrence.getKingdomKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.phylumKey, occurrence.getPhylumKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.classKey, occurrence.getClassKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.orderKey, occurrence.getOrderKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.familyKey, occurrence.getOrderKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.genusKey, occurrence.getGenusKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.subgenusKey, occurrence.getSubgenusKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.speciesKey, occurrence.getSpeciesKey()); dwcXMLDocument.append(DwcTerm.scientificName, occurrence.getScientificName()); dwcXMLDocument.append(DwcTerm.kingdom, occurrence.getKingdom()); dwcXMLDocument.append(DwcTerm.phylum, occurrence.getPhylum()); dwcXMLDocument.append(DwcTerm.class_, occurrence.getClazz()); dwcXMLDocument.append(DwcTerm.order, occurrence.getOrder()); dwcXMLDocument.append(DwcTerm.family, occurrence.getFamily()); dwcXMLDocument.append(DwcTerm.genus, occurrence.getGenus()); dwcXMLDocument.append(DwcTerm.subgenus, occurrence.getSubgenus()); dwcXMLDocument.append(GbifTerm.species, occurrence.getSpecies()); dwcXMLDocument.append(GbifTerm.genericName, occurrence.getGenericName()); dwcXMLDocument.append(DwcTerm.specificEpithet, occurrence.getSpecificEpithet()); dwcXMLDocument.append(DwcTerm.infraspecificEpithet, occurrence.getInfraspecificEpithet()); appendIfNotNull(dwcXMLDocument, DwcTerm.taxonRank, occurrence.getTaxonRank()); dwcXMLDocument.append(DwcTerm.dateIdentified, toISODateTime(occurrence.getDateIdentified())); appendIfNotNull(dwcXMLDocument, DwcTerm.decimalLatitude, occurrence.getDecimalLatitude()); appendIfNotNull(dwcXMLDocument, DwcTerm.decimalLongitude, occurrence.getDecimalLongitude()); appendIfNotNull(dwcXMLDocument, GbifTerm.coordinateAccuracy, occurrence.getCoordinateAccuracy()); appendIfNotNull(dwcXMLDocument, GbifTerm.elevation, occurrence.getElevation()); appendIfNotNull(dwcXMLDocument, GbifTerm.elevationAccuracy, occurrence.getElevationAccuracy()); appendIfNotNull(dwcXMLDocument, GbifTerm.depth, occurrence.getDepth()); appendIfNotNull(dwcXMLDocument, GbifTerm.depthAccuracy, occurrence.getDepthAccuracy()); appendIfNotNull(dwcXMLDocument, DwcTerm.continent, occurrence.getContinent()); appendDwcCountry(dwcXMLDocument, occurrence.getCountry()); dwcXMLDocument.append(DwcTerm.stateProvince, occurrence.getStateProvince()); dwcXMLDocument.append(DwcTerm.waterBody, occurrence.getWaterBody()); appendIfNotNull(dwcXMLDocument, DwcTerm.year, occurrence.getYear()); appendIfNotNull(dwcXMLDocument, DwcTerm.month, occurrence.getMonth()); appendIfNotNull(dwcXMLDocument, DwcTerm.day, occurrence.getDay()); dwcXMLDocument.append(DwcTerm.eventDate, toISODateTime(occurrence.getEventDate())); appendIfNotNull(dwcXMLDocument, DwcTerm.typeStatus, occurrence.getTypeStatus()); dwcXMLDocument.append(GbifTerm.typifiedName, occurrence.getTypifiedName()); dwcXMLDocument.append(DcTerm.modified, toISODateTime(occurrence.getModified())); dwcXMLDocument.append(GbifTerm.lastInterpreted, toISODateTime(occurrence.getLastInterpreted())); appendIfNotNull(dwcXMLDocument, DcTerm.references, occurrence.getReferences()); appendIfNotNull(dwcXMLDocument, GbifTerm.datasetKey, occurrence.getDatasetKey()); //append(dwcXMLDocument, GbifTerm., occurrence.getPublishingOrgKey()); appendIfNotNull(dwcXMLDocument, GbifTerm.protocol, occurrence.getProtocol()); dwcXMLDocument.append(GbifTerm.lastCrawled, toISODateTime(occurrence.getLastCrawled())); dwcXMLDocument.append(GbifTerm.lastParsed, toISODateTime(occurrence.getLastParsed())); for (OccurrenceIssue issue : occurrence.getIssues()) { dwcXMLDocument.append(GbifTerm.issue, issue.toString()); } // handle verbatim values for (Term term : occurrence.getVerbatimFields().keySet()) { dwcXMLDocument.tryAppend(term, occurrence.getVerbatimField(term)); } Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); DOMSource source = new DOMSource(dwcXMLDocument.getDocument()); StreamResult result = new StreamResult(baos); transformer.transform(source, result); } catch (ParserConfigurationException | TransformerException e) { LOG.error("Can't generate Dwc XML for Occurrence [{}]", occurrence); throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR); } return baos.toByteArray(); } @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return type == Occurrence.class; } @Override public long getSize(Occurrence occurrence, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { // deprecated by JAX-RS 2.0 and ignored by Jersey runtime return -1L; } @Override public void writeTo(Occurrence occurrence, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { entityStream.write(occurrenceXMLAsByteArray(occurrence)); } private static void appendDwcCountry(DwcXMLDocument dwcXMLDocument, Country value) { if (value != null) { dwcXMLDocument.append(DwcTerm.countryCode, value.getIso2LetterCode()); dwcXMLDocument.append(DwcTerm.country, value.getTitle()); } } /** * Specific appendIfNotNull for the supported {@link Term} implementation {@link DwcTerm}. * * @param dwcXMLDocument * @param term * @param value nulls accepted and skipped */ private static void appendIfNotNull(DwcXMLDocument dwcXMLDocument, DwcTerm term, Object value) { if (value != null) { dwcXMLDocument.append(term, value.toString()); } } /** * Specific appendIfNotNull for the supported {@link Term} implementation {@link DcTerm}. * * @param dwcXMLDocument * @param term * @param value nulls accepted and skipped */ private static void appendIfNotNull(DwcXMLDocument dwcXMLDocument, DcTerm term, Object value) { if (value != null) { dwcXMLDocument.append(term, value.toString()); } } /** * Specific appendIfNotNull for the supported {@link Term} implementation {@link GbifTerm}. * * @param dwcXMLDocument * @param term * @param value nulls accepted and skipped */ private static void appendIfNotNull(DwcXMLDocument dwcXMLDocument, GbifTerm term, Object value) { if (value != null) { dwcXMLDocument.append(term, value.toString()); } } private static String toISODateTime(Date date) { if (date == null) { return null; } return FDF.format(date); } }