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}