{"action":"created","comment":{"url":"https://api.github.com/repos/codice/ddf/pulls/comments/182233501","pull_request_review_id":112994213,"id":182233501,"diff_hunk":"@@ -0,0 +1,673 @@\n+/**\n+ * Copyright (c) Codice Foundation\n+ *\n+ * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU\n+ * Lesser General Public License as published by the Free Software Foundation, either version 3 of\n+ * the License, or any later version.\n+ *\n+ * <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n+ * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public\n+ * License is distributed along with this program and can be found at\n+ * <http://www.gnu.org/licenses/lgpl.html>.\n+ */\n+package org.codice.ddf.spatial.ogc.wfs.transformer.handlebars;\n+\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.B;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.BYTES_PER_GB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.BYTES_PER_KB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.BYTES_PER_MB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.BYTES_PER_PB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.BYTES_PER_TB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.GB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.KB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.MB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.PB;\n+import static org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants.TB;\n+\n+import ddf.catalog.data.Attribute;\n+import ddf.catalog.data.AttributeDescriptor;\n+import ddf.catalog.data.AttributeType;\n+import ddf.catalog.data.Metacard;\n+import ddf.catalog.data.MetacardType;\n+import ddf.catalog.data.impl.AttributeImpl;\n+import ddf.catalog.data.impl.MetacardImpl;\n+import ddf.catalog.data.types.Core;\n+import java.io.InputStream;\n+import java.io.Serializable;\n+import java.io.StringWriter;\n+import java.io.UnsupportedEncodingException;\n+import java.math.BigDecimal;\n+import java.net.URI;\n+import java.net.URISyntaxException;\n+import java.util.Collections;\n+import java.util.Date;\n+import java.util.HashMap;\n+import java.util.Iterator;\n+import java.util.List;\n+import java.util.Map;\n+import java.util.Objects;\n+import java.util.Optional;\n+import java.util.stream.Collectors;\n+import javax.xml.bind.DatatypeConverter;\n+import javax.xml.namespace.QName;\n+import javax.xml.stream.XMLEventFactory;\n+import javax.xml.stream.XMLEventReader;\n+import javax.xml.stream.XMLEventWriter;\n+import javax.xml.stream.XMLInputFactory;\n+import javax.xml.stream.XMLOutputFactory;\n+import javax.xml.stream.XMLStreamException;\n+import javax.xml.stream.events.Namespace;\n+import javax.xml.stream.events.StartElement;\n+import javax.xml.stream.events.XMLEvent;\n+import ogc.schema.opengis.wfs_capabilities.v_1_0_0.FeatureTypeType;\n+import org.apache.commons.lang3.StringEscapeUtils;\n+import org.apache.commons.lang3.StringUtils;\n+import org.apache.tika.io.IOUtils;\n+import org.boon.json.JsonException;\n+import org.boon.json.JsonFactory;\n+import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants;\n+import org.codice.ddf.spatial.ogc.wfs.catalog.metacardtype.registry.WfsMetacardTypeRegistry;\n+import org.codice.ddf.spatial.ogc.wfs.featuretransformer.FeatureTransformer;\n+import org.codice.ddf.spatial.ogc.wfs.featuretransformer.WfsMetadata;\n+import org.codice.ddf.transformer.xml.streaming.Gml3ToWkt;\n+import org.codice.ddf.transformer.xml.streaming.impl.Gml3ToWktImpl;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+import org.springframework.util.CollectionUtils;\n+\n+public class HandlebarsWfsFeatureTransformer implements FeatureTransformer<FeatureTypeType> {\n+\n+  private static final Logger LOGGER =\n+      LoggerFactory.getLogger(HandlebarsWfsFeatureTransformer.class);\n+\n+  private static final XMLInputFactory XML_INPUT_FACTORY = XMLInputFactory.newInstance();\n+\n+  private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance();\n+\n+  private static final XMLEventFactory XML_EVENT_FACTORY = XMLEventFactory.newInstance();\n+\n+  static final String ATTRIBUTE_NAME = \"attributeName\";\n+\n+  static final String FEATURE_NAME = \"featureName\";\n+\n+  static final String TEMPLATE = \"template\";\n+\n+  private static final String UTF8_ENCODING = \"UTF-8\";\n+\n+  private static final String METACARD_ID = \"metacardId\";\n+\n+  private static final String GML_NAMESPACE = \"http://www.opengis.net/gml\";\n+\n+  private String featureType;\n+\n+  private QName featureTypeQName;\n+\n+  private String dataUnit;\n+\n+  private MetacardType metacardType;\n+\n+  private Map<String, FeatureAttributeEntry> mappingEntries;\n+\n+  private WfsMetacardTypeRegistry metacardTypeRegistry;\n+\n+  public HandlebarsWfsFeatureTransformer() {\n+    mappingEntries = new HashMap<>();\n+  }\n+\n+  @Override\n+  public Optional<Metacard> apply(InputStream inputStream, WfsMetadata metadata) {\n+    if (!isStateValid(inputStream, metadata)) {\n+      LOGGER.debug(\"Transformer state is invalid: {}, {}\", featureType, mappingEntries);\n+      return Optional.empty();\n+    }\n+    lookupMetacardType(metadata);\n+    if (metacardType == null) {\n+      return Optional.empty();\n+    }\n+\n+    Map<String, String> contextMap = new HashMap<>();\n+    populateContextMap(inputStream, contextMap);\n+    if (CollectionUtils.isEmpty(contextMap)) {\n+      return Optional.empty();\n+    }\n+\n+    MetacardImpl metacard = (MetacardImpl) createMetacard(contextMap);\n+\n+    String id = null;\n+    if (StringUtils.isBlank(metacard.getId())) {\n+      id = contextMap.get(METACARD_ID);\n+      if (StringUtils.isNotBlank(id)) {\n+        metacard.setId(id);\n+      } else {\n+        LOGGER.debug(\"Feature id is blank. Unable to set metacard id.\");\n+      }\n+    }\n+\n+    metacard.setSourceId(metadata.getId());\n+\n+    Date date = new Date();\n+    if (metacard.getEffectiveDate() == null) {\n+      metacard.setEffectiveDate(date);\n+    }\n+    if (metacard.getCreatedDate() == null) {\n+      metacard.setCreatedDate(date);\n+    }\n+    if (metacard.getModifiedDate() == null) {\n+      metacard.setModifiedDate(date);\n+    }\n+\n+    if (StringUtils.isBlank(metacard.getTitle())) {\n+      metacard.setTitle(id);\n+    }\n+    metacard.setContentTypeName(metacardType.getName());\n+    try {\n+      metacard.setTargetNamespace(\n+          new URI(WfsConstants.NAMESPACE_URN_ROOT + metacardType.getName()));\n+    } catch (URISyntaxException e) {\n+      LOGGER.debug(\n+          \"Unable to set Target Namespace on metacard: {}.\",\n+          WfsConstants.NAMESPACE_URN_ROOT + metacardType.getName(),\n+          e);\n+    }\n+\n+    return Optional.of(metacard);\n+  }\n+\n+  /**\n+   * Reads in the FeatureMember from the inputstream, populating the contextMap with the XML tag\n+   * names and values\n+   *\n+   * @param inputStream the stream containing the FeatureMember xml document\n+   */\n+  private void populateContextMap(InputStream inputStream, Map<String, String> contextMap) {\n+    Map<String, String> namespaces = new HashMap<>();\n+    boolean canHandleFeatureType = false;\n+    try {\n+      XMLEventReader xmlEventReader = getXmlEventReader(inputStream);\n+\n+      String elementName = null;\n+      while (xmlEventReader.hasNext()) {\n+        XMLEvent xmlEvent = xmlEventReader.nextEvent();\n+        if (xmlEvent.isStartElement()) {\n+          StartElement startElement = xmlEvent.asStartElement();\n+          elementName = startElement.getName().getLocalPart();\n+          canHandleFeatureType |=\n+              processStartElement(\n+                  xmlEventReader, startElement, namespaces, contextMap, canHandleFeatureType);\n+        } else if (xmlEvent.isCharacters()) {\n+          contextMap.put(elementName, xmlEvent.asCharacters().getData());\n+        }\n+      }\n+      if (!canHandleFeatureType) {\n+        contextMap.clear();\n+      }\n+    } catch (XMLStreamException e) {\n+      LOGGER.debug(\"Error transforming feature to metacard.\", e);\n+    }\n+  }\n+\n+  private boolean processStartElement(\n+      XMLEventReader xmlEventReader,\n+      StartElement startElement,\n+      Map<String, String> namespaces,\n+      Map<String, String> contextMap,\n+      boolean featureTypeFound)\n+      throws XMLStreamException {\n+    mapNamespaces(startElement, namespaces);\n+    if (!featureTypeFound) {\n+      if (canHandleFeatureType(startElement)) {\n+        String id =\n+            getIdAttributeValue(\n+                startElement, namespaces, getNamespaceAlias(GML_NAMESPACE, namespaces));\n+        contextMap.put(METACARD_ID, id);\n+        return true;\n+      }\n+    } else {\n+      XMLEvent eventPeek = xmlEventReader.peek();\n+      if (eventPeek.isStartElement() && isGmlElement(eventPeek.asStartElement(), namespaces)) {\n+        readGmlData(xmlEventReader, startElement.getName().getLocalPart(), contextMap);\n+      }\n+    }\n+    return false;\n+  }\n+\n+  private boolean isGmlElement(StartElement startElement, Map<String, String> namespaces) {\n+    String gmlNamespaceAlias = getNamespaceAlias(GML_NAMESPACE, namespaces);\n+    if (StringUtils.isBlank(gmlNamespaceAlias)) {\n+      return false;\n+    }\n+    return startElement.getName().getPrefix().equals(gmlNamespaceAlias);\n+  }\n+\n+  private boolean canHandleFeatureType(StartElement startElement) {\n+    return startElement.getName().getLocalPart().equals(featureTypeQName.getLocalPart());\n+  }\n+\n+  private String getNamespaceAlias(String namespace, Map<String, String> namespaces) {\n+    return namespaces\n+        .entrySet()\n+        .stream()\n+        .filter(entry -> entry.getValue().contains(namespace))\n+        .map(Map.Entry::getKey)\n+        .findFirst()\n+        .orElse(\"\");\n+  }\n+\n+  private String getIdAttributeValue(\n+      StartElement startElement, Map<String, String> namespaces, String namespaceAlias) {\n+    String id = null;\n+    startElement.getAttributeByName(new QName(namespaces.get(namespaceAlias), \"id\")).getValue();\n+    javax.xml.stream.events.Attribute idAttribute =\n+        startElement.getAttributeByName(new QName(namespaces.get(namespaceAlias), \"id\"));\n+    if (idAttribute != null) {\n+      id = idAttribute.getValue();\n+    }\n+\n+    if (StringUtils.isBlank(id)) {\n+      for (Iterator i = startElement.getAttributes(); i.hasNext(); ) {\n+        idAttribute = (javax.xml.stream.events.Attribute) i.next();\n+        if (idAttribute != null && idAttribute.getName().getLocalPart().equals(\"id\")) {\n+          id = idAttribute.getValue();\n+        }\n+      }\n+    }\n+\n+    return id;\n+  }\n+\n+  private XMLEventReader getXmlEventReader(InputStream inputStream) throws XMLStreamException {\n+    XMLEventReader xmlEventReader = XML_INPUT_FACTORY.createXMLEventReader(inputStream);\n+    xmlEventReader =\n+        XML_INPUT_FACTORY.createFilteredReader(\n+            xmlEventReader,\n+            event -> {\n+              if (event.isCharacters()) {\n+                return event.asCharacters().getData().trim().length() > 0;\n+              }\n+\n+              return true;\n+            });\n+    return xmlEventReader;\n+  }\n+\n+  private void readGmlData(\n+      XMLEventReader xmlEventReader, String elementName, Map<String, String> contextMap)\n+      throws XMLStreamException {\n+    int count = 0;\n+    XMLEventWriter eventWriter = null;\n+    StringWriter stringWriter = new StringWriter();\n+    boolean readData = true;\n+\n+    try {\n+      while (readData) {\n+        XMLEvent xmlEvent = xmlEventReader.nextEvent();\n+        if (xmlEvent.isStartElement()) {\n+          if (count == 0) {\n+            eventWriter = XML_OUTPUT_FACTORY.createXMLEventWriter(stringWriter);\n+          }\n+          eventWriter.add(addNamespacesToStartElement(xmlEvent.asStartElement()));\n+\n+          count++;\n+        } else if (xmlEvent.isEndElement()) {\n+          if (count == 0) {\n+            if (eventWriter != null) {\n+              eventWriter.flush();\n+            }\n+            readData = false;\n+          } else {\n+            eventWriter.add(xmlEvent);\n+          }\n+          count--;\n+        } else {\n+          if (eventWriter != null) {\n+            eventWriter.add(xmlEvent);\n+          }\n+        }\n+      }\n+    } finally {\n+      if (eventWriter != null) {\n+        eventWriter.close();\n+      }\n+      IOUtils.closeQuietly(stringWriter);\n+    }\n+\n+    LOGGER.debug(\"String writer: {}\", stringWriter);\n+    contextMap.put(elementName, stringWriter.toString());\n+  }\n+\n+  private void lookupMetacardType(WfsMetadata metadata) {\n+    Optional<MetacardType> optionalMetacardType =\n+        metacardTypeRegistry.lookupMetacardTypeBySimpleName(\n+            metadata.getId(), featureTypeQName.getLocalPart());\n+    if (optionalMetacardType.isPresent()) {\n+      metacardType = optionalMetacardType.get();\n+    } else {\n+      LOGGER.debug(\n+          \"Error looking up metacard type for source id: '{}', and simple name: '{}'\",\n+          metadata.getId(),\n+          featureTypeQName.getLocalPart());\n+    }\n+  }\n+\n+  private Metacard createMetacard(Map<String, String> contextMap) {\n+    MetacardImpl metacard = new MetacardImpl(metacardType);\n+\n+    List<Attribute> attributes =\n+        mappingEntries\n+            .values()\n+            .stream()\n+            .map(entry -> createAttribute(entry, contextMap))\n+            .filter(Objects::nonNull)\n+            .collect(Collectors.toList());\n+\n+    attributes.forEach(metacard::setAttribute);\n+\n+    return metacard;\n+  }\n+\n+  private StartElement addNamespacesToStartElement(StartElement startElement) {\n+    String prefix = startElement.getName().getPrefix();\n+    if (StringUtils.isBlank(prefix)) {\n+      return startElement;\n+    }\n+\n+    if (isPrefixBound(startElement, prefix)) {\n+      return startElement;\n+    }\n+\n+    return XML_EVENT_FACTORY.createStartElement(\n+        prefix,\n+        startElement.getNamespaceURI(prefix),\n+        startElement.getName().getLocalPart(),\n+        startElement.getAttributes(),\n+        Collections.singletonList(\n+                XML_EVENT_FACTORY.createNamespace(prefix, startElement.getNamespaceURI(prefix)))\n+            .iterator());\n+  }\n+\n+  private Attribute createAttribute(FeatureAttributeEntry entry, Map<String, String> contextMap) {\n+    String value;\n+    if (StringUtils.isNotBlank(entry.getTemplateText())) {\n+      value = entry.getMappingFunction().apply(contextMap);\n+    } else {\n+      value = contextMap.get(entry.getFeatureProperty());\n+    }\n+\n+    if (StringUtils.isBlank(value)) {\n+      LOGGER.debug(\"No value found for feature type: {}\", entry.getFeatureProperty());\n+      return null;\n+    }\n+\n+    Serializable attributeValue = getMetacardAttributeValue(entry.getFeatureProperty(), value);\n+    if (attributeValue == null) {\n+      LOGGER.debug(\n+          \"No attribute value found for feature type: {}, attribute: {}\",\n+          entry.getFeatureProperty(),\n+          entry.getAttributeName());\n+      return null;\n+    }\n+\n+    return new AttributeImpl(entry.getAttributeName(), attributeValue);\n+  }\n+\n+  private Serializable getMetacardAttributeValue(String featureName, String featureValue) {\n+    FeatureAttributeEntry entry = mappingEntries.get(featureName);\n+    if (entry == null) {\n+      LOGGER.debug(\n+          \"Error handling feature name: {}, with value: {}. No mapping entry found for feature name\",\n+          featureName,\n+          featureValue);\n+      return null;\n+    }\n+\n+    AttributeDescriptor attributeDescriptor =\n+        metacardType.getAttributeDescriptor(entry.getAttributeName());\n+    if (attributeDescriptor == null) {\n+      LOGGER.debug(\n+          \"AttributeDescriptor for attribute name {} not found. The mapping is being ignored.\",\n+          entry.getAttributeName());\n+      return null;\n+    }\n+\n+    Serializable attributeValue = null;\n+\n+    if (StringUtils.equals(entry.getAttributeName(), Core.RESOURCE_SIZE)) {\n+      String bytes = convertToBytes(featureValue, getDataUnit());\n+\n+      if (StringUtils.isNotBlank(bytes)) {\n+        attributeValue = bytes;\n+      }\n+    } else {\n+      attributeValue =\n+          getValueForAttributeFormat(\n+              attributeDescriptor.getType().getAttributeFormat(), featureValue);\n+    }\n+\n+    return attributeValue;\n+  }\n+\n+  private Serializable getValueForAttributeFormat(\n+      AttributeType.AttributeFormat attributeFormat, String value) {\n+\n+    Serializable serializable = null;\n+    switch (attributeFormat) {\n+      case BOOLEAN:\n+        serializable = Boolean.valueOf(value);\n+        break;\n+      case DOUBLE:\n+        serializable = Double.valueOf(value);\n+        break;\n+      case FLOAT:\n+        serializable = Float.valueOf(value);\n+        break;\n+      case INTEGER:\n+        serializable = Integer.valueOf(value);\n+        break;\n+      case LONG:\n+        serializable = Long.valueOf(value);\n+        break;\n+      case SHORT:\n+        serializable = Short.valueOf(value);\n+        break;\n+      case XML:\n+      case STRING:\n+        serializable = value;\n+        break;\n+      case GEOMETRY:\n+        LOGGER.trace(\"Unescape the geometry: {}\", value);\n+        value = StringEscapeUtils.unescapeXml(value);\n+        LOGGER.debug(\"Geometry value after it has been xml unescaped: {}\", value);\n+        String wkt = getWktFromGeometry(value);\n+        LOGGER.debug(\"String wkt value: {}\", wkt);\n+        serializable = wkt;\n+        break;\n+      case BINARY:\n+        try {\n+          serializable = value.getBytes(UTF8_ENCODING);\n+        } catch (UnsupportedEncodingException e) {\n+          LOGGER.debug(\"Error encoding the binary value into the metacard.\", e);\n+        }\n+        break;\n+      case DATE:\n+        try {\n+          serializable = DatatypeConverter.parseDate(value).getTime();\n+        } catch (IllegalArgumentException e) {\n+          LOGGER.debug(\"Error converting value to a date. value: '{}'\", value, e);\n+        }\n+        break;\n+      default:\n+        break;\n+    }\n+    return serializable;\n+  }\n+\n+  private String getWktFromGeometry(String geometry) {\n+    String wkt = getWktFromGml3(geometry);\n+    if (StringUtils.isNotBlank(wkt)) {\n+      return wkt;\n+    }\n+    LOGGER.debug(\"Error converting gml to wkt using gml3 configuration. Trying gml2.\", geometry);\n+    return getWktFromGml2(geometry);\n+  }\n+\n+  private String getWktFromGml3(String geometry) {\n+    String wkt = null;\n+    Gml3ToWkt gml3ToWkt = new Gml3ToWktImpl(new org.geotools.gml3.GMLConfiguration());\n+    try {\n+      wkt = gml3ToWkt.convert(geometry);\n+    } catch (Exception e) {\n+      LOGGER.debug(\"Error converting gml to wkt using gml3 configuration. GML: {}.\", geometry, e);\n+    }\n+    return wkt;\n+  }\n+\n+  private String getWktFromGml2(String geometry) {\n+    String wkt = null;\n+    Gml3ToWkt gml3ToWkt = new Gml3ToWktImpl(new org.geotools.gml2.GMLConfiguration());\n+    try {\n+      wkt = gml3ToWkt.convert(geometry);\n+    } catch (Exception e) {\n+      LOGGER.debug(\"Error converting gml to wkt using gml2 configuration. GML: {}.\", geometry, e);\n+    }\n+    return wkt;\n+  }\n+\n+  private boolean isPrefixBound(StartElement startElement, String prefix) {\n+    for (Iterator i = startElement.getNamespaces(); i.hasNext(); ) {\n+      Namespace namespace = (Namespace) i.next();\n+\n+      if (namespace.getPrefix().equals(prefix)) {\n+        return true;\n+      }\n+    }\n+    return false;\n+  }\n+\n+  private void mapNamespaces(StartElement startElement, Map<String, String> map) {\n+\n+    for (Iterator i = startElement.getNamespaces(); i.hasNext(); ) {\n+      Namespace namespace = (Namespace) i.next();\n+      map.put(namespace.getPrefix(), namespace.getNamespaceURI());\n+    }\n+  }\n+\n+  private boolean isStateValid(InputStream inputStream, WfsMetadata metadata) {\n+    if (inputStream == null) {\n+      LOGGER.debug(\"Received a null input stream.\");\n+      return false;\n+    }\n+\n+    if (metadata == null) {\n+      LOGGER.debug(\"Received a null WfsMetadata object.\");\n+      return false;\n+    }\n+\n+    if (StringUtils.isBlank(featureType)) {\n+      LOGGER.debug(\"Feature type must contain a value: {}\", featureType);\n+      return false;\n+    }\n+\n+    if (CollectionUtils.isEmpty(mappingEntries.values())) {\n+      LOGGER.debug(\"There are no mappings for feature type: {}\", featureType);\n+      return false;\n+    }\n+\n+    return true;\n+  }\n+\n+  private void addAttributeMapping(String attributeName, String featureName, String templateText) {\n+    LOGGER.trace(\n+        \"Adding attribute mapping from: {} to: {} using: {}\",\n+        attributeName,\n+        featureName,\n+        templateText);\n+    mappingEntries.put(\n+        featureName, new FeatureAttributeEntry(attributeName, featureName, templateText));\n+  }\n+\n+  public String getDataUnit() {\n+    return dataUnit;\n+  }\n+\n+  public void setDataUnit(String unit) {\n+    LOGGER.trace(\"Setting data unit to: {}\", unit);\n+    dataUnit = unit;\n+  }\n+\n+  public void setFeatureType(String featureType) {\n+    LOGGER.trace(\"Setting feature type to: {}\", featureType);\n+    this.featureType = featureType;\n+    featureTypeQName = QName.valueOf(featureType);\n+  }\n+\n+  public void setMetacardTypeRegistry(WfsMetacardTypeRegistry metacardTypeRegistry) {\n+    this.metacardTypeRegistry = metacardTypeRegistry;\n+  }\n+\n+  /**\n+   * Sets a list of attribute mappings from a list of JSON strings.\n+   *\n+   * @param attributeMappingsList - a list of JSON-formatted `FeatureAttributeEntry` objects.\n+   */\n+  public void setAttributeMappings(/*@Nullable*/ List<String> attributeMappingsList) {\n+    LOGGER.trace(\"Setting attribute mappings to: {}\", attributeMappingsList);\n+    if (attributeMappingsList != null) {\n+      mappingEntries.clear();\n+      attributeMappingsList\n+          .stream()\n+          .filter(StringUtils::isNotEmpty)\n+          .map(\n+              string -> {\n+                try {\n+                  return JsonFactory.create().readValue(string, Map.class);\n+                } catch (JsonException e) {\n+                  LOGGER.debug(\"Failed to parse attribute mapping json '{}'\", string, e);\n+                }\n+                return null;\n+              })\n+          .filter(Objects::nonNull)\n+          .filter(map -> map.get(ATTRIBUTE_NAME) instanceof String)\n+          .filter(map -> map.get(FEATURE_NAME) instanceof String)\n+          .filter(map -> map.get(TEMPLATE) instanceof String)\n+          .forEach(\n+              map ->\n+                  addAttributeMapping(\n+                      (String) map.get(ATTRIBUTE_NAME),\n+                      (String) map.get(FEATURE_NAME),\n+                      (String) map.get(TEMPLATE)));\n+    }\n+  }\n+\n+  private String convertToBytes(String value, String unit) {\n+\n+    BigDecimal resourceSize = new BigDecimal(value);","path":"catalog/spatial/wfs/spatial-wfs-featuretransformer-handlebars/src/main/java/org/codice/ddf/spatial/ogc/wfs/transformer/handlebars/HandlebarsWfsFeatureTransformer.java","position":644,"original_position":644,"commit_id":"ceb9a9f3227f32a8d3c3c8b6d02fa5f83e8e509c","original_commit_id":"ceb9a9f3227f32a8d3c3c8b6d02fa5f83e8e509c","user":{"login":"bdeining","id":6551038,"avatar_url":"https://avatars2.githubusercontent.com/u/6551038?v=4","gravatar_id":"","url":"https://api.github.com/users/bdeining","html_url":"https://github.com/bdeining","followers_url":"https://api.github.com/users/bdeining/followers","following_url":"https://api.github.com/users/bdeining/following{/other_user}","gists_url":"https://api.github.com/users/bdeining/gists{/gist_id}","starred_url":"https://api.github.com/users/bdeining/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bdeining/subscriptions","organizations_url":"https://api.github.com/users/bdeining/orgs","repos_url":"https://api.github.com/users/bdeining/repos","events_url":"https://api.github.com/users/bdeining/events{/privacy}","received_events_url":"https://api.github.com/users/bdeining/received_events","type":"User","site_admin":false},"body":"❓ what's the rationale behind using `BigDecimal`?","created_at":"2018-04-17T21:05:35Z","updated_at":"2018-04-17T21:16:14Z","html_url":"https://github.com/codice/ddf/pull/3130#discussion_r182233501","pull_request_url":"https://api.github.com/repos/codice/ddf/pulls/3130","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/codice/ddf/pulls/comments/182233501"},"html":{"href":"https://github.com/codice/ddf/pull/3130#discussion_r182233501"},"pull_request":{"href":"https://api.github.com/repos/codice/ddf/pulls/3130"}}},"pull_request":{"url":"https://api.github.com/repos/codice/ddf/pulls/3130","id":180386622,"html_url":"https://github.com/codice/ddf/pull/3130","diff_url":"https://github.com/codice/ddf/pull/3130.diff","patch_url":"https://github.com/codice/ddf/pull/3130.patch","issue_url":"https://api.github.com/repos/codice/ddf/issues/3130","number":3130,"state":"open","locked":false,"title":"(wip) DDF-3573 Create FeatureTransformer API and Implementation","user":{"login":"dcruver","id":5922390,"avatar_url":"https://avatars1.githubusercontent.com/u/5922390?v=4","gravatar_id":"","url":"https://api.github.com/users/dcruver","html_url":"https://github.com/dcruver","followers_url":"https://api.github.com/users/dcruver/followers","following_url":"https://api.github.com/users/dcruver/following{/other_user}","gists_url":"https://api.github.com/users/dcruver/gists{/gist_id}","starred_url":"https://api.github.com/users/dcruver/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcruver/subscriptions","organizations_url":"https://api.github.com/users/dcruver/orgs","repos_url":"https://api.github.com/users/dcruver/repos","events_url":"https://api.github.com/users/dcruver/events{/privacy}","received_events_url":"https://api.github.com/users/dcruver/received_events","type":"User","site_admin":false},"body":"What does this PR do?\r\nAdded bundles for FeatureTransformer API and implementation. Updated\r\nWFS 1.1 source to use the FeatureTransformer API. This is a work-in-\r\nprogress. Items that remain to be done:\r\n\r\nFix Broken Unit Tests\r\nUpdate Documentation\r\nFix Checkstyle Errors\r\nRename spatial-wfs-message-api bundle\r\nRename spatial-wfs-message bundle\r\nMove org.codice.ddf.spatial.ogc.wfs.catalog.message.api to org.codice.ddf.spatial.ogc.wfs.catalog.message\r\nMove org.codice.ddf.spatial.ogc.wfs.catalog.message to org.codice.ddf.spatial.ogc.wfs.catalog.message.impl\r\n\r\nWho is reviewing it?\r\n@troymohl\r\n@mojogitoverhere\r\n@BernardIgiri\r\n@GabrielFabian\r\n\r\n(please choose AT LEAST two reviewers that need to approve the PR before it can get merged)\r\n\r\nSelect relevant component teams:\r\nhttps://github.com/orgs/codice/teams\r\n\r\nChoose 2 committers to review/merge the PR.\r\n(please choose ONLY two committers from below, delete the rest)\r\n@jlcsmith\r\n@pklinef\r\n\r\nHow should this be tested? (List steps with links to updated documentation)\r\nAny background context you want to provide?\r\nWhat are the relevant tickets?\r\n\r\nScreenshots (if appropriate)\r\nChecklist:\r\n[-] Documentation Updated\r\n[-] Update / Add Unit Tests\r\n[-] Update / Add Integration Tests\r\nNotes on Review Process\r\nPlease see Notes on Review Process for further guidance on requirements for merging and abbreviated reviews.\r\n\r\nReview Comment Legend:\r\n:pencil2:  (Pencil) This comment is a nitpick or style suggestion, no action required for approval. This comment should provide a suggestion either as an in line code snippet or a gist.\r\n:question:  (Question Mark) This comment is to gain a clearer understanding of design or code choices, clarification is required but action may not be necessary for approval.\r\n:exclamation:  (Exclamation Mark) This comment is critical and requires clarification or action before approval.","created_at":"2018-04-09T18:15:34Z","updated_at":"2018-04-17T21:16:14Z","closed_at":null,"merged_at":null,"merge_commit_sha":"43bd1aa800f07d5411e662a128c9ee8dab552892","assignee":{"login":"pklinef","id":700518,"avatar_url":"https://avatars1.githubusercontent.com/u/700518?v=4","gravatar_id":"","url":"https://api.github.com/users/pklinef","html_url":"https://github.com/pklinef","followers_url":"https://api.github.com/users/pklinef/followers","following_url":"https://api.github.com/users/pklinef/following{/other_user}","gists_url":"https://api.github.com/users/pklinef/gists{/gist_id}","starred_url":"https://api.github.com/users/pklinef/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pklinef/subscriptions","organizations_url":"https://api.github.com/users/pklinef/orgs","repos_url":"https://api.github.com/users/pklinef/repos","events_url":"https://api.github.com/users/pklinef/events{/privacy}","received_events_url":"https://api.github.com/users/pklinef/received_events","type":"User","site_admin":false},"assignees":[{"login":"pklinef","id":700518,"avatar_url":"https://avatars1.githubusercontent.com/u/700518?v=4","gravatar_id":"","url":"https://api.github.com/users/pklinef","html_url":"https://github.com/pklinef","followers_url":"https://api.github.com/users/pklinef/followers","following_url":"https://api.github.com/users/pklinef/following{/other_user}","gists_url":"https://api.github.com/users/pklinef/gists{/gist_id}","starred_url":"https://api.github.com/users/pklinef/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pklinef/subscriptions","organizations_url":"https://api.github.com/users/pklinef/orgs","repos_url":"https://api.github.com/users/pklinef/repos","events_url":"https://api.github.com/users/pklinef/events{/privacy}","received_events_url":"https://api.github.com/users/pklinef/received_events","type":"User","site_admin":false},{"login":"jlcsmith","id":4503545,"avatar_url":"https://avatars3.githubusercontent.com/u/4503545?v=4","gravatar_id":"","url":"https://api.github.com/users/jlcsmith","html_url":"https://github.com/jlcsmith","followers_url":"https://api.github.com/users/jlcsmith/followers","following_url":"https://api.github.com/users/jlcsmith/following{/other_user}","gists_url":"https://api.github.com/users/jlcsmith/gists{/gist_id}","starred_url":"https://api.github.com/users/jlcsmith/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jlcsmith/subscriptions","organizations_url":"https://api.github.com/users/jlcsmith/orgs","repos_url":"https://api.github.com/users/jlcsmith/repos","events_url":"https://api.github.com/users/jlcsmith/events{/privacy}","received_events_url":"https://api.github.com/users/jlcsmith/received_events","type":"User","site_admin":false}],"requested_reviewers":[{"login":"stustison","id":4391679,"avatar_url":"https://avatars0.githubusercontent.com/u/4391679?v=4","gravatar_id":"","url":"https://api.github.com/users/stustison","html_url":"https://github.com/stustison","followers_url":"https://api.github.com/users/stustison/followers","following_url":"https://api.github.com/users/stustison/following{/other_user}","gists_url":"https://api.github.com/users/stustison/gists{/gist_id}","starred_url":"https://api.github.com/users/stustison/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stustison/subscriptions","organizations_url":"https://api.github.com/users/stustison/orgs","repos_url":"https://api.github.com/users/stustison/repos","events_url":"https://api.github.com/users/stustison/events{/privacy}","received_events_url":"https://api.github.com/users/stustison/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/codice/ddf/pulls/3130/commits","review_comments_url":"https://api.github.com/repos/codice/ddf/pulls/3130/comments","review_comment_url":"https://api.github.com/repos/codice/ddf/pulls/comments{/number}","comments_url":"https://api.github.com/repos/codice/ddf/issues/3130/comments","statuses_url":"https://api.github.com/repos/codice/ddf/statuses/ceb9a9f3227f32a8d3c3c8b6d02fa5f83e8e509c","head":{"label":"dcruver:DDF-3573-2","ref":"DDF-3573-2","sha":"ceb9a9f3227f32a8d3c3c8b6d02fa5f83e8e509c","user":{"login":"dcruver","id":5922390,"avatar_url":"https://avatars1.githubusercontent.com/u/5922390?v=4","gravatar_id":"","url":"https://api.github.com/users/dcruver","html_url":"https://github.com/dcruver","followers_url":"https://api.github.com/users/dcruver/followers","following_url":"https://api.github.com/users/dcruver/following{/other_user}","gists_url":"https://api.github.com/users/dcruver/gists{/gist_id}","starred_url":"https://api.github.com/users/dcruver/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcruver/subscriptions","organizations_url":"https://api.github.com/users/dcruver/orgs","repos_url":"https://api.github.com/users/dcruver/repos","events_url":"https://api.github.com/users/dcruver/events{/privacy}","received_events_url":"https://api.github.com/users/dcruver/received_events","type":"User","site_admin":false},"repo":{"id":42196026,"name":"ddf","full_name":"dcruver/ddf","owner":{"login":"dcruver","id":5922390,"avatar_url":"https://avatars1.githubusercontent.com/u/5922390?v=4","gravatar_id":"","url":"https://api.github.com/users/dcruver","html_url":"https://github.com/dcruver","followers_url":"https://api.github.com/users/dcruver/followers","following_url":"https://api.github.com/users/dcruver/following{/other_user}","gists_url":"https://api.github.com/users/dcruver/gists{/gist_id}","starred_url":"https://api.github.com/users/dcruver/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcruver/subscriptions","organizations_url":"https://api.github.com/users/dcruver/orgs","repos_url":"https://api.github.com/users/dcruver/repos","events_url":"https://api.github.com/users/dcruver/events{/privacy}","received_events_url":"https://api.github.com/users/dcruver/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/dcruver/ddf","description":"The Distributed Data Framework - Distribution, SDK, and Test","fork":true,"url":"https://api.github.com/repos/dcruver/ddf","forks_url":"https://api.github.com/repos/dcruver/ddf/forks","keys_url":"https://api.github.com/repos/dcruver/ddf/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dcruver/ddf/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dcruver/ddf/teams","hooks_url":"https://api.github.com/repos/dcruver/ddf/hooks","issue_events_url":"https://api.github.com/repos/dcruver/ddf/issues/events{/number}","events_url":"https://api.github.com/repos/dcruver/ddf/events","assignees_url":"https://api.github.com/repos/dcruver/ddf/assignees{/user}","branches_url":"https://api.github.com/repos/dcruver/ddf/branches{/branch}","tags_url":"https://api.github.com/repos/dcruver/ddf/tags","blobs_url":"https://api.github.com/repos/dcruver/ddf/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dcruver/ddf/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dcruver/ddf/git/refs{/sha}","trees_url":"https://api.github.com/repos/dcruver/ddf/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dcruver/ddf/statuses/{sha}","languages_url":"https://api.github.com/repos/dcruver/ddf/languages","stargazers_url":"https://api.github.com/repos/dcruver/ddf/stargazers","contributors_url":"https://api.github.com/repos/dcruver/ddf/contributors","subscribers_url":"https://api.github.com/repos/dcruver/ddf/subscribers","subscription_url":"https://api.github.com/repos/dcruver/ddf/subscription","commits_url":"https://api.github.com/repos/dcruver/ddf/commits{/sha}","git_commits_url":"https://api.github.com/repos/dcruver/ddf/git/commits{/sha}","comments_url":"https://api.github.com/repos/dcruver/ddf/comments{/number}","issue_comment_url":"https://api.github.com/repos/dcruver/ddf/issues/comments{/number}","contents_url":"https://api.github.com/repos/dcruver/ddf/contents/{+path}","compare_url":"https://api.github.com/repos/dcruver/ddf/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dcruver/ddf/merges","archive_url":"https://api.github.com/repos/dcruver/ddf/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dcruver/ddf/downloads","issues_url":"https://api.github.com/repos/dcruver/ddf/issues{/number}","pulls_url":"https://api.github.com/repos/dcruver/ddf/pulls{/number}","milestones_url":"https://api.github.com/repos/dcruver/ddf/milestones{/number}","notifications_url":"https://api.github.com/repos/dcruver/ddf/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dcruver/ddf/labels{/name}","releases_url":"https://api.github.com/repos/dcruver/ddf/releases{/id}","deployments_url":"https://api.github.com/repos/dcruver/ddf/deployments","created_at":"2015-09-09T18:15:55Z","updated_at":"2016-04-13T23:29:12Z","pushed_at":"2018-04-17T17:05:48Z","git_url":"git://github.com/dcruver/ddf.git","ssh_url":"git@github.com:dcruver/ddf.git","clone_url":"https://github.com/dcruver/ddf.git","svn_url":"https://github.com/dcruver/ddf","homepage":"","size":196442,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"codice:master","ref":"master","sha":"1a97d00dc33f7654d3929b281e9184281fc1a076","user":{"login":"codice","id":2386734,"avatar_url":"https://avatars2.githubusercontent.com/u/2386734?v=4","gravatar_id":"","url":"https://api.github.com/users/codice","html_url":"https://github.com/codice","followers_url":"https://api.github.com/users/codice/followers","following_url":"https://api.github.com/users/codice/following{/other_user}","gists_url":"https://api.github.com/users/codice/gists{/gist_id}","starred_url":"https://api.github.com/users/codice/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codice/subscriptions","organizations_url":"https://api.github.com/users/codice/orgs","repos_url":"https://api.github.com/users/codice/repos","events_url":"https://api.github.com/users/codice/events{/privacy}","received_events_url":"https://api.github.com/users/codice/received_events","type":"Organization","site_admin":false},"repo":{"id":15173563,"name":"ddf","full_name":"codice/ddf","owner":{"login":"codice","id":2386734,"avatar_url":"https://avatars2.githubusercontent.com/u/2386734?v=4","gravatar_id":"","url":"https://api.github.com/users/codice","html_url":"https://github.com/codice","followers_url":"https://api.github.com/users/codice/followers","following_url":"https://api.github.com/users/codice/following{/other_user}","gists_url":"https://api.github.com/users/codice/gists{/gist_id}","starred_url":"https://api.github.com/users/codice/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codice/subscriptions","organizations_url":"https://api.github.com/users/codice/orgs","repos_url":"https://api.github.com/users/codice/repos","events_url":"https://api.github.com/users/codice/events{/privacy}","received_events_url":"https://api.github.com/users/codice/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/codice/ddf","description":"DDF Distributed Data Framework - an open source, modular integration framework.","fork":false,"url":"https://api.github.com/repos/codice/ddf","forks_url":"https://api.github.com/repos/codice/ddf/forks","keys_url":"https://api.github.com/repos/codice/ddf/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codice/ddf/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codice/ddf/teams","hooks_url":"https://api.github.com/repos/codice/ddf/hooks","issue_events_url":"https://api.github.com/repos/codice/ddf/issues/events{/number}","events_url":"https://api.github.com/repos/codice/ddf/events","assignees_url":"https://api.github.com/repos/codice/ddf/assignees{/user}","branches_url":"https://api.github.com/repos/codice/ddf/branches{/branch}","tags_url":"https://api.github.com/repos/codice/ddf/tags","blobs_url":"https://api.github.com/repos/codice/ddf/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codice/ddf/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codice/ddf/git/refs{/sha}","trees_url":"https://api.github.com/repos/codice/ddf/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codice/ddf/statuses/{sha}","languages_url":"https://api.github.com/repos/codice/ddf/languages","stargazers_url":"https://api.github.com/repos/codice/ddf/stargazers","contributors_url":"https://api.github.com/repos/codice/ddf/contributors","subscribers_url":"https://api.github.com/repos/codice/ddf/subscribers","subscription_url":"https://api.github.com/repos/codice/ddf/subscription","commits_url":"https://api.github.com/repos/codice/ddf/commits{/sha}","git_commits_url":"https://api.github.com/repos/codice/ddf/git/commits{/sha}","comments_url":"https://api.github.com/repos/codice/ddf/comments{/number}","issue_comment_url":"https://api.github.com/repos/codice/ddf/issues/comments{/number}","contents_url":"https://api.github.com/repos/codice/ddf/contents/{+path}","compare_url":"https://api.github.com/repos/codice/ddf/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codice/ddf/merges","archive_url":"https://api.github.com/repos/codice/ddf/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codice/ddf/downloads","issues_url":"https://api.github.com/repos/codice/ddf/issues{/number}","pulls_url":"https://api.github.com/repos/codice/ddf/pulls{/number}","milestones_url":"https://api.github.com/repos/codice/ddf/milestones{/number}","notifications_url":"https://api.github.com/repos/codice/ddf/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codice/ddf/labels{/name}","releases_url":"https://api.github.com/repos/codice/ddf/releases{/id}","deployments_url":"https://api.github.com/repos/codice/ddf/deployments","created_at":"2013-12-13T20:38:28Z","updated_at":"2018-04-17T16:49:54Z","pushed_at":"2018-04-17T19:40:15Z","git_url":"git://github.com/codice/ddf.git","ssh_url":"git@github.com:codice/ddf.git","clone_url":"https://github.com/codice/ddf.git","svn_url":"https://github.com/codice/ddf","homepage":"http://ddf.codice.org","size":389316,"stargazers_count":79,"watchers_count":79,"language":"Java","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":136,"mirror_url":null,"archived":false,"open_issues_count":41,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":136,"open_issues":41,"watchers":79,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/codice/ddf/pulls/3130"},"html":{"href":"https://github.com/codice/ddf/pull/3130"},"issue":{"href":"https://api.github.com/repos/codice/ddf/issues/3130"},"comments":{"href":"https://api.github.com/repos/codice/ddf/issues/3130/comments"},"review_comments":{"href":"https://api.github.com/repos/codice/ddf/pulls/3130/comments"},"review_comment":{"href":"https://api.github.com/repos/codice/ddf/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/codice/ddf/pulls/3130/commits"},"statuses":{"href":"https://api.github.com/repos/codice/ddf/statuses/ceb9a9f3227f32a8d3c3c8b6d02fa5f83e8e509c"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"bdeining"}}