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