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