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.jackson; 015 016import org.gbif.api.model.occurrence.DownloadFormat; 017import org.gbif.api.model.occurrence.DownloadRequest; 018import org.gbif.api.model.occurrence.DownloadType; 019import org.gbif.api.model.occurrence.PredicateDownloadRequest; 020import org.gbif.api.model.occurrence.SqlDownloadRequest; 021import org.gbif.api.model.predicate.Predicate; 022import org.gbif.api.util.VocabularyUtils; 023import org.gbif.api.vocabulary.Extension; 024 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collections; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Optional; 032import java.util.Set; 033import java.util.stream.Collectors; 034 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import com.fasterxml.jackson.core.JsonParser; 039import com.fasterxml.jackson.databind.DeserializationContext; 040import com.fasterxml.jackson.databind.JsonDeserializer; 041import com.fasterxml.jackson.databind.JsonNode; 042import com.fasterxml.jackson.databind.ObjectMapper; 043 044/** 045 * Download request deserializer. 046 * <p> 047 * For most of the time, the serialization has been to "notificationAddresses" and 048 * "sendNotification". For a few months in 2018-2019, it was "notification_address" and 049 * "send_notification". 050 * <p> 051 * The API documentation has previously specified "notification_address" and "sendNotification". 052 * <p> 053 * We therefore accept all combinations. 054 * <p> 055 * https://github.com/gbif/portal-feedback/issues/2046 056 */ 057public class DownloadRequestSerde extends JsonDeserializer<DownloadRequest> { 058 059 private static final String PREDICATE = "predicate"; 060 private static final List<String> SEND_NOTIFICATION = Collections 061 .unmodifiableList(Arrays.asList("sendNotification", "send_notification")); 062 private static final String SQL = "sql"; 063 private static final List<String> NOTIFICATION_ADDRESSES = 064 Collections.unmodifiableList( 065 Arrays.asList("notificationAddresses", "notificationAddress", "notification_addresses", 066 "notification_address")); 067 private static final String CREATOR = "creator"; 068 private static final String FORMAT = "format"; 069 private static final String TYPE = "type"; 070 private static final String VERBATIM_EXTENSIONS = "verbatimExtensions"; 071 072 // Properties we ignore. 073 private static final List<String> IGNORED_PROPERTIES = 074 Collections.unmodifiableList( 075 Arrays.asList("created")); 076 077 private static final Set<String> ALL_PROPERTIES; 078 private static final Logger LOG = LoggerFactory.getLogger(DownloadRequestSerde.class); 079 private static final ObjectMapper MAPPER = new ObjectMapper(); 080 081 static { 082 Set<String> allProperties = new HashSet<>(Arrays.asList(PREDICATE, SQL, CREATOR, FORMAT, TYPE, VERBATIM_EXTENSIONS)); 083 allProperties.addAll(SEND_NOTIFICATION); 084 allProperties.addAll(NOTIFICATION_ADDRESSES); 085 allProperties.addAll(IGNORED_PROPERTIES); 086 ALL_PROPERTIES = Collections.unmodifiableSet(allProperties); 087 } 088 089 @Override 090 public DownloadRequest deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { 091 JsonNode node = jp.getCodec().readTree(jp); 092 LOG.debug("DownloadRequest for deserialization: {}", node); 093 //at least one element must be defined 094 if (node.size() == 0) { 095 return null; 096 } 097 DownloadFormat format = Optional.ofNullable(node.get(FORMAT)) 098 .map(n -> VocabularyUtils.lookupEnum(n.asText(), DownloadFormat.class)).orElse(DownloadFormat.DWCA); 099 100 DownloadType type = Optional.ofNullable(node.get(TYPE)) 101 .map(n -> VocabularyUtils.lookupEnum(n.asText(), DownloadType.class)).orElse(DownloadType.OCCURRENCE); 102 103 String creator = Optional.ofNullable(node.get(CREATOR)).map(JsonNode::asText).orElse(null); 104 105 List<String> notificationAddresses = new ArrayList<>(); 106 for (final String jsonKey : NOTIFICATION_ADDRESSES) { 107 notificationAddresses.addAll(Optional.ofNullable(node.get(jsonKey)).map(jsonNode -> { 108 try { 109 return Arrays.asList(MAPPER.treeToValue(jsonNode, String[].class)); 110 } catch (Exception e) { 111 throw new RuntimeException(e); 112 } 113 }).orElse(new ArrayList<>())); 114 } 115 116 boolean sendNotification = false; 117 for (final String jsonKey : SEND_NOTIFICATION) { 118 sendNotification |= Optional.ofNullable(node.get(jsonKey)).map(JsonNode::asBoolean).orElse(Boolean.FALSE); 119 } 120 121 Set<Extension> extensions = Optional.ofNullable(node.get(VERBATIM_EXTENSIONS)).map(jsonNode -> { 122 try { 123 return Arrays.stream(MAPPER.treeToValue(jsonNode, String[].class)) 124 .map(Extension::fromRowType) 125 .collect(Collectors.toSet()); 126 } catch (Exception e) { 127 throw new RuntimeException(e); 128 } 129 }).orElse(Collections.emptySet()); 130 131 // Check requested extensions are available for download. 132 for (Extension e : extensions) { 133 if (!Extension.availableExtensions().contains(e)) { 134 throw new RuntimeException("The "+e.getRowType()+" extension is not available for downloads."); 135 } 136 } 137 138 String sql = Optional.ofNullable(node.get(SQL)).map(JsonNode::asText).orElse(null); 139 140 // Reject if unknown field names are present 141 // https://github.com/gbif/occurrence/issues/273 142 node.fieldNames().forEachRemaining(n -> { if (!ALL_PROPERTIES.contains(n)) { throw new RuntimeException("Unknown JSON property '"+n+"'."); }}); 143 144 if (sql != null) { 145 if (format != DownloadFormat.SQL_TSV_ZIP) { 146 throw new RuntimeException("SQL downloads must use a suitable download format: SQL_TSV_ZIP."); 147 } 148 return new SqlDownloadRequest(sql, creator, notificationAddresses, sendNotification, type, format); 149 } else { 150 if (format == DownloadFormat.SQL_TSV_ZIP) { 151 throw new RuntimeException("Predicate downloads must not use an SQL download format."); 152 } 153 JsonNode predicate = Optional.ofNullable(node.get(PREDICATE)).orElse(null); 154 // Not yet enforced, we would need e.g. http://api.gbif.org/v1/occurrence/download/request/predicate?format=DWCA 155 // to return 'predicate: {}' etc. 156 //if (predicate == null) { 157 // throw new RuntimeException("A predicate must be specified. Use {} for everything."); 158 //} 159 Predicate predicateObj = predicate == null ? null : MAPPER.treeToValue(predicate, Predicate.class); 160 return new PredicateDownloadRequest(predicateObj, creator, notificationAddresses, sendNotification, format, type, extensions); 161 } 162 } 163}