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 077 // Properties we ignore. 078 private static final List<String> IGNORED_PROPERTIES = 079 Collections.unmodifiableList( 080 Arrays.asList("created")); 081 082 private static final Set<String> ALL_PROPERTIES; 083 private static final Logger LOG = LoggerFactory.getLogger(DownloadRequestSerde.class); 084 private static final ObjectMapper MAPPER = new ObjectMapper(); 085 086 static { 087 Set<String> allProperties = new HashSet<>(Arrays.asList(PREDICATE, SQL, CREATOR, FORMAT, TYPE, VERBATIM_EXTENSIONS, DESCRIPTION, MACHINE_DESCRIPTION)); 088 allProperties.addAll(SEND_NOTIFICATION); 089 allProperties.addAll(NOTIFICATION_ADDRESSES); 090 allProperties.addAll(IGNORED_PROPERTIES); 091 ALL_PROPERTIES = Collections.unmodifiableSet(allProperties); 092 } 093 094 @Override 095 public DownloadRequest deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { 096 JsonNode node = jp.getCodec().readTree(jp); 097 LOG.debug("DownloadRequest for deserialization: {}", node); 098 //at least one element must be defined 099 if (node.size() == 0) { 100 return null; 101 } 102 DownloadFormat format = Optional.ofNullable(node.get(FORMAT)) 103 .map(n -> VocabularyUtils.lookupEnum(n.asText(), DownloadFormat.class)).orElse(DownloadFormat.DWCA); 104 105 DownloadType type = Optional.ofNullable(node.get(TYPE)) 106 .map(n -> VocabularyUtils.lookupEnum(n.asText(), DownloadType.class)).orElse(DownloadType.OCCURRENCE); 107 108 String creator = Optional.ofNullable(node.get(CREATOR)).map(JsonNode::asText).orElse(null); 109 110 String description = Optional.ofNullable(node.get(DESCRIPTION)).map(JsonNode::asText).orElse(null); 111 112 List<String> notificationAddresses = new ArrayList<>(); 113 for (final String jsonKey : NOTIFICATION_ADDRESSES) { 114 notificationAddresses.addAll(Optional.ofNullable(node.get(jsonKey)).map(jsonNode -> { 115 try { 116 return Arrays.asList(MAPPER.treeToValue(jsonNode, String[].class)); 117 } catch (Exception e) { 118 throw new RuntimeException(e); 119 } 120 }).orElse(new ArrayList<>())); 121 } 122 123 boolean sendNotification = false; 124 for (final String jsonKey : SEND_NOTIFICATION) { 125 sendNotification |= Optional.ofNullable(node.get(jsonKey)).map(JsonNode::asBoolean).orElse(Boolean.FALSE); 126 } 127 128 Set<Extension> extensions = Optional.ofNullable(node.get(VERBATIM_EXTENSIONS)).map(jsonNode -> { 129 try { 130 return Arrays.stream(MAPPER.treeToValue(jsonNode, String[].class)) 131 .map(Extension::fromRowType) 132 .collect(Collectors.toSet()); 133 } catch (Exception e) { 134 throw new RuntimeException(e); 135 } 136 }).orElse(Collections.emptySet()); 137 138 // Check requested extensions are available for download. 139 for (Extension e : extensions) { 140 if (!Extension.availableExtensions().contains(e)) { 141 throw new RuntimeException("The "+e.getRowType()+" extension is not available for downloads."); 142 } 143 } 144 145 JsonNode machineDescription = Optional.ofNullable(node.get(MACHINE_DESCRIPTION)).orElse(null); 146 147 String sql = Optional.ofNullable(node.get(SQL)).map(JsonNode::asText).orElse(null); 148 149 // Reject if unknown field names are present 150 // https://github.com/gbif/occurrence/issues/273 151 node.fieldNames().forEachRemaining(n -> { if (!ALL_PROPERTIES.contains(n)) { throw new RuntimeException("Unknown JSON property '"+n+"'."); }}); 152 153 if (sql != null) { 154 if (format != DownloadFormat.SQL_TSV_ZIP) { 155 throw new RuntimeException("SQL downloads must use a suitable download format: SQL_TSV_ZIP."); 156 } 157 return new SqlDownloadRequest(sql, creator, notificationAddresses, sendNotification, format, type, description, machineDescription); 158 } else { 159 if (format == DownloadFormat.SQL_TSV_ZIP) { 160 throw new RuntimeException("Predicate downloads must not use an SQL download format."); 161 } 162 JsonNode predicate = Optional.ofNullable(node.get(PREDICATE)).orElse(null); 163 // Not yet enforced, we would need e.g. http://api.gbif.org/v1/occurrence/download/request/predicate?format=DWCA 164 // to return 'predicate: {}' etc. 165 //if (predicate == null) { 166 // throw new RuntimeException("A predicate must be specified. Use {} for everything."); 167 //} 168 MAPPER.registerModule( 169 new SimpleModule().addDeserializer( 170 OccurrenceSearchParameter.class, 171 new OccurrenceSearchParameter.OccurrenceSearchParameterDeserializer() 172 ) 173 ); 174 175 Predicate predicateObj = predicate == null ? null : MAPPER.treeToValue(predicate, Predicate.class); 176 return new PredicateDownloadRequest(predicateObj, creator, notificationAddresses, sendNotification, format, type, description, machineDescription, extensions); 177 } 178 } 179}