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