/** * 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 org.codice.ddf.spatial.ogc.csw.catalog.converter; import java.io.Serializable; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.apache.commons.lang.StringUtils; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswAxisOrder; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswConstants; import org.codice.ddf.spatial.ogc.csw.catalog.common.converter.DefaultCswRecordMap; import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.CswTransactionRequest; import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.DeleteAction; import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.InsertAction; import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.UpdateAction; import org.codice.ddf.spatial.ogc.csw.catalog.common.transformer.TransformerManager; import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import ddf.catalog.data.Attribute; import ddf.catalog.data.AttributeDescriptor; import ddf.catalog.data.AttributeRegistry; import ddf.catalog.data.Metacard; import ddf.catalog.data.impl.MetacardImpl; import net.opengis.cat.csw.v_2_0_2.DeleteType; import net.opengis.cat.csw.v_2_0_2.QueryConstraintType; public class TransactionRequestConverter implements Converter { private static JAXBContext jaxBContext; private Converter delegatingTransformer; private CswRecordConverter cswRecordConverter; private AttributeRegistry registry; public TransactionRequestConverter(Converter itp, AttributeRegistry registry) { this.delegatingTransformer = itp; this.registry = registry; } public CswRecordConverter getCswRecordConverter() { return this.cswRecordConverter; } public void setCswRecordConverter(CswRecordConverter cswRecordConverter) { this.cswRecordConverter = cswRecordConverter; } @Override public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext marshallingContext) { if (o == null || !CswTransactionRequest.class.isAssignableFrom(o.getClass())) { return; } CswTransactionRequest request = (CswTransactionRequest) o; writer.addAttribute(CswConstants.SERVICE, request.getService()); writer.addAttribute(CswConstants.VERSION, request.getVersion()); writer.addAttribute(CswConstants.VERBOSE_RESPONSE, String.valueOf(request.isVerbose())); writer.addAttribute(CswConstants.XMLNS + CswConstants.NAMESPACE_DELIMITER + CswConstants.CSW_NAMESPACE_PREFIX, CswConstants.CSW_OUTPUT_SCHEMA); writer.addAttribute(CswConstants.XMLNS + CswConstants.NAMESPACE_DELIMITER + CswConstants.OGC_NAMESPACE_PREFIX, CswConstants.OGC_SCHEMA); for (InsertAction insertAction : request.getInsertActions()) { writer.startNode(CswConstants.CSW_TRANSACTION_INSERT_NODE); writer.addAttribute(CswConstants.TYPE_NAME_PARAMETER, insertAction.getTypeName()); marshallingContext.put(CswConstants.TRANSFORMER_LOOKUP_KEY, TransformerManager.ID); marshallingContext.put(CswConstants.TRANSFORMER_LOOKUP_VALUE, insertAction.getTypeName()); for (Metacard metacard : insertAction.getRecords()) { marshallingContext.convertAnother(metacard, delegatingTransformer); } writer.endNode(); } for (UpdateAction updateAction : request.getUpdateActions()) { writer.startNode(CswConstants.CSW_TRANSACTION_UPDATE_NODE); writer.addAttribute(CswConstants.TYPE_NAME_PARAMETER, updateAction.getTypeName()); marshallingContext.put(CswConstants.TRANSFORMER_LOOKUP_KEY, TransformerManager.ID); marshallingContext.put(CswConstants.TRANSFORMER_LOOKUP_VALUE, updateAction.getTypeName()); marshallingContext.convertAnother(updateAction.getMetacard(), delegatingTransformer); writer.endNode(); } for (DeleteAction deleteAction : request.getDeleteActions()) { writer.startNode(CswConstants.CSW_TRANSACTION_DELETE_NODE); writer.addAttribute(CswConstants.TYPE_NAME_PARAMETER, deleteAction.getTypeName()); writer.startNode(CswConstants.CSW_CONSTRAINT); writer.addAttribute(CswConstants.VERSION, CswConstants.CONSTRAINT_VERSION); writer.startNode(CswConstants.CSW_CQL_TEXT); writer.setValue(deleteAction.getConstraint() .getCqlText()); writer.endNode(); writer.endNode(); writer.endNode(); } } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { CswTransactionRequest cswTransactionRequest = new CswTransactionRequest(); cswTransactionRequest.setVersion(reader.getAttribute(CswConstants.VERSION)); cswTransactionRequest.setService(reader.getAttribute(CswConstants.SERVICE)); cswTransactionRequest.setVerbose(Boolean.valueOf(reader.getAttribute(CswConstants.VERBOSE_RESPONSE))); XStreamAttributeCopier.copyXmlNamespaceDeclarationsIntoContext(reader, context); while (reader.hasMoreChildren()) { reader.moveDown(); if (reader.getNodeName() .contains("Insert")) { String typeName = StringUtils.defaultIfEmpty(reader.getAttribute(CswConstants.TYPE_NAME_PARAMETER), CswConstants.CSW_RECORD); String handle = StringUtils.defaultIfEmpty(reader.getAttribute(CswConstants.HANDLE_PARAMETER), ""); context.put(CswConstants.TRANSFORMER_LOOKUP_KEY, TransformerManager.ID); context.put(CswConstants.TRANSFORMER_LOOKUP_VALUE, typeName); List<Metacard> metacards = new ArrayList<>(); // Loop through the individual records to be inserted, converting each into a Metacard while (reader.hasMoreChildren()) { reader.moveDown(); // move down to the record's tag Metacard metacard = (Metacard) context.convertAnother(null, MetacardImpl.class, delegatingTransformer); if (metacard != null) { metacards.add(metacard); } // move back up to the <SearchResults> parent of the <csw:Record> tags reader.moveUp(); } cswTransactionRequest.getInsertActions() .add(new InsertAction(typeName, handle, metacards)); } else if (reader.getNodeName() .contains("Delete")) { XStreamAttributeCopier.copyXmlNamespaceDeclarationsIntoContext(reader, context); Map<String, String> xmlnsAttributeToUriMappings = getXmlnsAttributeToUriMappingsFromContext(context); Map<String, String> prefixToUriMappings = getPrefixToUriMappingsFromXmlnsAttributes( xmlnsAttributeToUriMappings); StringWriter writer = new StringWriter(); XStreamAttributeCopier.copyXml(reader, writer, xmlnsAttributeToUriMappings); DeleteType deleteType = getElementFromXml(writer.toString(), DeleteType.class); cswTransactionRequest.getDeleteActions() .add(new DeleteAction(deleteType, prefixToUriMappings)); } else if (reader.getNodeName() .contains("Update")) { XStreamAttributeCopier.copyXmlNamespaceDeclarationsIntoContext(reader, context); UpdateAction updateAction = parseUpdateAction(reader, context); cswTransactionRequest.getUpdateActions() .add(updateAction); } reader.moveUp(); } return cswTransactionRequest; } private UpdateAction parseUpdateAction(HierarchicalStreamReader reader, UnmarshallingContext context) { Map<String, String> xmlnsAttributeToUriMappings = getXmlnsAttributeToUriMappingsFromContext( context); Map<String, String> prefixToUriMappings = getPrefixToUriMappingsFromXmlnsAttributes( xmlnsAttributeToUriMappings); String typeName = StringUtils.defaultIfEmpty(reader.getAttribute(CswConstants.TYPE_NAME_PARAMETER), CswConstants.CSW_RECORD); String handle = StringUtils.defaultIfEmpty(reader.getAttribute(CswConstants.HANDLE_PARAMETER), ""); // Move down to the content of the <Update>. reader.moveDown(); UpdateAction updateAction; // Do we have a list of <RecordProperty> elements or a new <csw:Record>? if (reader.getNodeName() .contains("RecordProperty")) { Map<String, Serializable> cswRecordProperties = new HashMap<>(); while (reader.getNodeName() .contains("RecordProperty")) { String cswField; Serializable newValue = null; // Move down to the <Name>. reader.moveDown(); if (reader.getNodeName() .contains("Name")) { String attribute = reader.getValue(); cswField = CswRecordConverter.getCswAttributeFromAttributeName(attribute); } else { throw new ConversionException( "Missing Parameter Value: missing a Name in a RecordProperty."); } // Move back up to the <RecordProperty>. reader.moveUp(); String attrName = DefaultCswRecordMap.getDefaultMetacardFieldForPrefixedString(cswField); cswRecordProperties.put(attrName, null); // Is there a <Value>? while (reader.hasMoreChildren()) { // Move down to the <Value>. reader.moveDown(); if (reader.getNodeName() .contains("Value")) { newValue = getRecordPropertyValue(reader, attrName); } else { throw new ConversionException( "Invalid Parameter Value: invalid element in a RecordProperty."); } Serializable currentValue = cswRecordProperties.get(attrName); if (currentValue != null) { if (currentValue instanceof List) { ((List) currentValue).add(newValue); } else { LinkedList<Serializable> list = new LinkedList<>(); list.add(currentValue); list.add(newValue); cswRecordProperties.put(attrName, list); } } else { cswRecordProperties.put(attrName, newValue); } // Back to the <RecordProperty>. reader.moveUp(); } // Back to the <Update>, look for the next <RecordProperty>. reader.moveUp(); if (!reader.hasMoreChildren()) { // If there aren't any more children of the <Update>, that means there's no // Constraint, which is required. throw new ConversionException("Missing Parameter Value: missing a Constraint."); } // What's the next element in the <Update>? reader.moveDown(); } // Now there should be a <Constraint> element. if (reader.getNodeName() .contains("Constraint")) { StringWriter writer = new StringWriter(); XStreamAttributeCopier.copyXml(reader, writer, xmlnsAttributeToUriMappings); QueryConstraintType constraint = getElementFromXml(writer.toString(), QueryConstraintType.class); // For any CSW attributes that map to basic metacard attributes (e.g. title, // modified date, etc.), update the basic metacard attributes as well. Map<String, String> cswToMetacardAttributeNames = DefaultCswRecordMap.getDefaultCswRecordMap() .getCswToMetacardAttributeNames(); Map<String, Serializable> cswRecordPropertiesWithMetacardAttributes = new HashMap<>( cswRecordProperties); for (Entry<String, Serializable> recordProperty : cswRecordProperties.entrySet()) { String cswAttributeName = recordProperty.getKey(); // If this CSW attribute maps to a basic metacard attribute, attempt to set the // basic metacard attribute. if (cswToMetacardAttributeNames.containsKey(cswAttributeName)) { String metacardAttrName = cswToMetacardAttributeNames.get(cswAttributeName); // If this basic metacard attribute hasn't already been set, set it. if (!cswRecordPropertiesWithMetacardAttributes.containsKey(metacardAttrName)) { Attribute metacardAttr = cswRecordConverter.getMetacardAttributeFromCswAttribute( cswAttributeName, recordProperty.getValue(), metacardAttrName); cswRecordPropertiesWithMetacardAttributes.put(metacardAttrName, metacardAttr.getValue()); } } } updateAction = new UpdateAction(cswRecordPropertiesWithMetacardAttributes, typeName, handle, constraint, prefixToUriMappings); } else { throw new ConversionException("Missing Parameter Value: missing a Constraint."); } } else { context.put(CswConstants.TRANSFORMER_LOOKUP_KEY, TransformerManager.ID); context.put(CswConstants.TRANSFORMER_LOOKUP_VALUE, typeName); Metacard metacard = (Metacard) context.convertAnother(null, MetacardImpl.class, delegatingTransformer); updateAction = new UpdateAction(metacard, typeName, handle); // Move back to the <Update>. reader.moveUp(); } return updateAction; } private Serializable getRecordPropertyValue(HierarchicalStreamReader reader, String cswField) { try { Serializable newValue; if (reader.hasMoreChildren()) { reader.moveDown(); newValue = readPropertyValue(reader, cswField); reader.moveUp(); } else { newValue = readPropertyValue(reader, cswField); } return newValue; } catch (NumberFormatException e) { throw new ConversionException("Invalid Parameter Value: a RecordProperty " + "specified a Value that does not match the type " + cswField + " expected by for the field " + cswField, e); } } private Serializable readPropertyValue(HierarchicalStreamReader reader, String cswField) { if (registry != null) { Optional<AttributeDescriptor> descriptor = registry.lookup(cswField); if (descriptor.isPresent()) { return CswUnmarshallHelper.convertRecordPropertyToMetacardAttribute(descriptor.get() .getType() .getAttributeFormat(), reader, CswAxisOrder.LON_LAT); } } // Assume the value is a String return reader.getValue(); } private <T> T getElementFromXml(String xml, Class<T> clazz) { JAXBElement<T> root; try { JAXBContext jaxbContext = getJaxBContext(); XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new StringReader(xml)); root = jaxbContext.createUnmarshaller() .unmarshal(xmlStreamReader, clazz); } catch (JAXBException | XMLStreamException e) { throw new ConversionException(e); } return root.getValue(); } public static synchronized JAXBContext getJaxBContext() throws JAXBException { if (jaxBContext == null) { List<String> contextList = Arrays.asList(net.opengis.cat.csw.v_2_0_2.ObjectFactory.class.getPackage() .getName(), net.opengis.filter.v_1_1_0.ObjectFactory.class.getPackage() .getName(), net.opengis.gml.v_3_1_1.ObjectFactory.class.getPackage() .getName(), net.opengis.ows.v_1_0_0.ObjectFactory.class.getPackage() .getName()); jaxBContext = JAXBContext.newInstance(StringUtils.join(contextList, ":")); } return jaxBContext; } private Map<String, String> getXmlnsAttributeToUriMappingsFromContext( UnmarshallingContext context) { Object namespaceObj = context.get(CswConstants.NAMESPACE_DECLARATIONS); if (namespaceObj instanceof Map<?, ?>) { return (Map<String, String>) namespaceObj; } return null; } private Map<String, String> getPrefixToUriMappingsFromXmlnsAttributes( Map<String, String> xmlnsAttributeToUriMappings) { if (xmlnsAttributeToUriMappings != null) { // The xmlns attributes on the top-level Transaction element have been copied // into the UnmarshallingContext by // XStreamAttributeCopier.copyXmlNamespaceDeclarationsIntoContext(). Map<String, String> prefixToUriMappings = new HashMap<>(); for (Entry<String, String> entry : xmlnsAttributeToUriMappings.entrySet()) { String xmlnsAttribute = entry.getKey(); if (StringUtils.contains(xmlnsAttribute, CswConstants.NAMESPACE_DELIMITER)) { String prefix = xmlnsAttribute.split(CswConstants.NAMESPACE_DELIMITER)[1]; prefixToUriMappings.put(prefix, entry.getValue()); } } return prefixToUriMappings; } return DefaultCswRecordMap.getDefaultCswRecordMap() .getPrefixToUriMapping(); } @Override public boolean canConvert(Class aClass) { return CswTransactionRequest.class.isAssignableFrom(aClass); } }