/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.transformer.input.geojson; import static java.util.stream.Collectors.toMap; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.isNotEmpty; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TimeZone; import java.util.function.Function; import javax.xml.bind.DatatypeConverter; import org.apache.commons.lang3.StringUtils; import org.boon.json.JsonFactory; import org.boon.json.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.AttributeDescriptor; import ddf.catalog.data.AttributeRegistry; import ddf.catalog.data.AttributeType.AttributeFormat; import ddf.catalog.data.Metacard; import ddf.catalog.data.MetacardType; import ddf.catalog.data.impl.MetacardImpl; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.transform.InputTransformer; import ddf.geo.formatter.CompositeGeometry; /** * Converts standard GeoJSON (geojson.org) into a Metacard. The limitation on the GeoJSON is that it * must conform to the {@link ddf.catalog.data.impl.BasicTypes#BASIC_METACARD} {@link MetacardType}. */ public class GeoJsonInputTransformer implements InputTransformer { static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; private static final ObjectMapper MAPPER = JsonFactory.create(); private static final String METACARD_TYPE_PROPERTY_KEY = "metacard-type"; private static final String ID = "geojson"; private static final String MIME_TYPE = "application/json"; private static final String SOURCE_ID_PROPERTY = "source-id"; private static final Logger LOGGER = LoggerFactory.getLogger(GeoJsonInputTransformer.class); private List<MetacardType> metacardTypes; private AttributeRegistry attributeRegistry; /** * Transforms GeoJson (http://www.geojson.org/) into a {@link Metacard} */ @Override public Metacard transform(InputStream input) throws IOException, CatalogTransformerException { return transform(input, null); } @Override public Metacard transform(InputStream input, String id) throws IOException, CatalogTransformerException { if (input == null) { throw new CatalogTransformerException("Cannot transform null input."); } Map<String, Object> rootObject = MAPPER.parser() .parseMap(input); if (rootObject == null) { throw new CatalogTransformerException("Unable to parse JSON for metacard."); } Object typeValue = rootObject.get(CompositeGeometry.TYPE_KEY); if (typeValue == null || !typeValue.equals("Feature")) { throw new CatalogTransformerException(new UnsupportedOperationException( "Only supported type is Feature, not [" + typeValue + "]")); } Map<String, Object> properties = (Map<String, Object>) rootObject.get(CompositeGeometry.PROPERTIES_KEY); if (properties == null) { throw new CatalogTransformerException("Properties are required to create a Metacard."); } final String propertyTypeName = (String) properties.get(METACARD_TYPE_PROPERTY_KEY); MetacardImpl metacard; if (isEmpty(propertyTypeName) || metacardTypes == null) { LOGGER.debug( "MetacardType specified in input is null or empty. Assuming default MetacardType"); metacard = new MetacardImpl(); } else { MetacardType metacardType = metacardTypes.stream() .filter(type -> type.getName() .equals(propertyTypeName)) .findFirst() .orElseThrow(() -> new CatalogTransformerException( "MetacardType specified in input has not been registered with the system. Cannot parse input. MetacardType name: " + propertyTypeName)); LOGGER.debug("Found registered MetacardType: {}", propertyTypeName); metacard = new MetacardImpl(metacardType); } MetacardType metacardType = metacard.getMetacardType(); LOGGER.debug("Metacard type name: {}", metacardType.getName()); // retrieve geometry Map<String, Object> geometryJson = (Map<String, Object>) rootObject.get(CompositeGeometry.GEOMETRY_KEY); CompositeGeometry geoJsonGeometry = null; if (geometryJson != null) { if (geometryJson.get(CompositeGeometry.TYPE_KEY) != null && (geometryJson.get( CompositeGeometry.COORDINATES_KEY) != null || geometryJson.get(CompositeGeometry.GEOMETRIES_KEY) != null)) { String geometryTypeJson = geometryJson.get(CompositeGeometry.TYPE_KEY) .toString(); geoJsonGeometry = CompositeGeometry.getCompositeGeometry(geometryTypeJson, geometryJson); } else { LOGGER.debug("Could not find geometry type."); } } if (geoJsonGeometry != null && StringUtils.isNotEmpty(geoJsonGeometry.toWkt())) { metacard.setLocation(geoJsonGeometry.toWkt()); } Map<String, AttributeDescriptor> attributeDescriptorMap = metacardType.getAttributeDescriptors() .stream() .collect(toMap(AttributeDescriptor::getName, Function.identity())); for (Map.Entry<String, Object> entry : properties.entrySet()) { final String key = entry.getKey(); final Object value = entry.getValue(); try { if (attributeDescriptorMap.containsKey(key)) { AttributeDescriptor ad = attributeDescriptorMap.get(key); metacard.setAttribute(key, convertProperty(value, ad)); } else { Optional<AttributeDescriptor> optional = attributeRegistry.lookup(key); if (optional.isPresent()) { metacard.setAttribute(key, convertProperty(value, optional.get())); } } } catch (NumberFormatException | ParseException e) { LOGGER.info( "GeoJSON input for attribute name '{}' does not match the expected AttributeType. This attribute will not be added to the metacard.", key, e); } } if (isNotEmpty(metacard.getSourceId())) { properties.put(SOURCE_ID_PROPERTY, metacard.getSourceId()); } if (id != null) { metacard.setId(id); } return metacard; } public void setMetacardTypes(List<MetacardType> metacardTypes) { this.metacardTypes = metacardTypes; } public void setAttributeRegistry(AttributeRegistry attributeRegistry) { this.attributeRegistry = attributeRegistry; } @Override public String toString() { return "InputTransformer {Impl=" + this.getClass() .getName() + ", id=" + ID + ", mime-type=" + MIME_TYPE + "}"; } private Serializable convertProperty(Object property, AttributeDescriptor descriptor) throws ParseException { AttributeFormat format = descriptor.getType() .getAttributeFormat(); if (descriptor.isMultiValued() && property instanceof List) { List<Serializable> values = new ArrayList<>(); for (Object value : (List) property) { values.add(convertValue(value, format)); } return (Serializable) values; } else { return convertValue(property, format); } } private Serializable convertValue(Object value, AttributeFormat format) throws ParseException { if (value == null) { return null; } switch (format) { case BINARY: return DatatypeConverter.parseBase64Binary(value.toString()); case DATE: SimpleDateFormat dateFormat = new SimpleDateFormat(ISO_8601_DATE_FORMAT); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat.parse(value.toString()); case GEOMETRY: case STRING: case XML: return value.toString(); case BOOLEAN: return Boolean.parseBoolean(value.toString()); case SHORT: return Short.parseShort(value.toString()); case INTEGER: return Integer.parseInt(value.toString()); case LONG: return Long.parseLong(value.toString()); case FLOAT: return Float.parseFloat(value.toString()); case DOUBLE: return Double.parseDouble(value.toString()); default: return null; } } }