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}