/**
* Copyright (c) Codice Foundation
*
* 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.
*
* 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.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.measure.converter.ConversionException;
import javax.xml.XMLConstants;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswConstants;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswRecordCollection;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Metacard;
import ddf.catalog.data.impl.MetacardImpl;
import net.opengis.cat.csw.v_2_0_2.ResultType;
/**
* Converts a {@link org.codice.ddf.spatial.ogc.csw.catalog.common.CswRecordCollection} into a
* {@link net.opengis.cat.csw.v_2_0_2.GetRecordsResponseType} with CSW records
*/
public class GetRecordsResponseConverter implements Converter {
private static final Logger LOGGER = LoggerFactory.getLogger(GetRecordsResponseConverter.class);
private static final String SEARCH_STATUS_NODE_NAME = "SearchStatus";
private static final String SEARCH_RESULTS_NODE_NAME = "SearchResults";
private static final String VERSION_ATTRIBUTE = "version";
private static final String TIMESTAMP_ATTRIBUTE = "timestamp";
private static final String NUMBER_OF_RECORDS_MATCHED_ATTRIBUTE = "numberOfRecordsMatched";
private static final String NUMBER_OF_RECORDS_RETURNED_ATTRIBUTE = "numberOfRecordsReturned";
private static final String NEXT_RECORD_ATTRIBUTE = "nextRecord";
private static final String RECORD_SCHEMA_ATTRIBUTE = "recordSchema";
private static final String ELEMENT_SET_ATTRIBUTE = "elementSet";
private Converter transformProvider;
private DefaultCswRecordMap defaultCswRecordMap = new DefaultCswRecordMap();
private String outputSchema = CswConstants.CSW_OUTPUT_SCHEMA;
/**
* Creates a new GetRecordsResponseConverter Object
*
* @param transformProvider The converter which will transform a {@link Metacard} to a the appropriate XML
* format and vice versa.
*/
public GetRecordsResponseConverter(Converter transformProvider) {
this.transformProvider = transformProvider;
}
@Override
public boolean canConvert(Class type) {
boolean canConvert = CswRecordCollection.class.isAssignableFrom(type);
LOGGER.debug("Can convert? {}", canConvert);
return canConvert;
}
@Override
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
LOGGER.debug("Entering GetRecordsResponseConverter.marshal()");
if (source == null || !(source instanceof CswRecordCollection)) {
LOGGER.warn("Failed to marshal CswRecordCollection: {}", source);
return;
}
CswRecordCollection cswRecordCollection = (CswRecordCollection) source;
for (Entry<String, String> entry : defaultCswRecordMap.getPrefixToUriMapping().entrySet()) {
writer.addAttribute(
XMLConstants.XMLNS_ATTRIBUTE + CswConstants.NAMESPACE_DELIMITER + entry
.getKey(), entry.getValue());
}
long start = 1;
String elementSet = null;
String recordSchema = CswConstants.CSW_OUTPUT_SCHEMA;
if (cswRecordCollection.getStartPosition() > 0) {
start = cswRecordCollection.getStartPosition();
}
if (StringUtils.isNotBlank(cswRecordCollection.getOutputSchema())) {
recordSchema = cswRecordCollection.getOutputSchema();
}
context.put(CswConstants.OUTPUT_SCHEMA_PARAMETER, recordSchema);
if (cswRecordCollection.getElementSetType() != null) {
context.put(CswConstants.ELEMENT_SET_TYPE, cswRecordCollection.getElementSetType());
elementSet = cswRecordCollection.getElementSetType().value();
} else if (cswRecordCollection.getElementName() != null) {
context.put(CswConstants.ELEMENT_NAMES, cswRecordCollection.getElementName());
}
long nextRecord = start + cswRecordCollection.getNumberOfRecordsReturned();
if (nextRecord > cswRecordCollection.getNumberOfRecordsMatched()) {
nextRecord = 0;
}
if (!cswRecordCollection.isById()) {
writer.addAttribute(VERSION_ATTRIBUTE, CswConstants.VERSION_2_0_2);
writer.startNode(CswConstants.CSW_NAMESPACE_PREFIX + CswConstants.NAMESPACE_DELIMITER
+ SEARCH_STATUS_NODE_NAME);
writer.addAttribute(TIMESTAMP_ATTRIBUTE,
ISODateTimeFormat.dateTime().print(new DateTime()));
writer.endNode();
writer.startNode(CswConstants.CSW_NAMESPACE_PREFIX + CswConstants.NAMESPACE_DELIMITER
+ SEARCH_RESULTS_NODE_NAME);
writer.addAttribute(NUMBER_OF_RECORDS_MATCHED_ATTRIBUTE,
Long.toString(cswRecordCollection.getNumberOfRecordsMatched()));
if (!ResultType.HITS.equals(cswRecordCollection.getResultType())) {
writer.addAttribute(NUMBER_OF_RECORDS_RETURNED_ATTRIBUTE,
Long.toString(cswRecordCollection.getNumberOfRecordsReturned()));
} else {
writer.addAttribute(NUMBER_OF_RECORDS_RETURNED_ATTRIBUTE, Long.toString(0));
}
writer.addAttribute(NEXT_RECORD_ATTRIBUTE, Long.toString(nextRecord));
writer.addAttribute(RECORD_SCHEMA_ATTRIBUTE, recordSchema);
if (StringUtils.isNotBlank(elementSet)) {
writer.addAttribute(ELEMENT_SET_ATTRIBUTE, elementSet);
}
}
context.put(CswConstants.WRITE_NAMESPACES, cswRecordCollection.isDoWriteNamespaces());
if (!ResultType.HITS.equals(cswRecordCollection.getResultType())) {
LOGGER.debug("Transforming individual metacards.");
for (Metacard mc : cswRecordCollection.getCswRecords()) {
context.convertAnother(mc, transformProvider);
}
}
if (!cswRecordCollection.isById()) {
writer.endNode();
}
}
/**
* Parses GetRecordsResponse XML of this form:
* <p/>
* <pre>
* {@code
* <csw:GetRecordsResponse xmlns:csw="http://www.opengis.net/cat/csw">
* <csw:SearchStatus status="subset" timestamp="2013-05-01T02:13:36+0200"/>
* <csw:SearchResults elementSet="full" nextRecord="11"
* numberOfRecordsMatched="479" numberOfRecordsReturned="10"
* recordSchema="csw:Record">
* <csw:Record xmlns:csw="http://www.opengis.net/cat/csw">
* ...
* </csw:Record>
* <csw:Record xmlns:csw="http://www.opengis.net/cat/csw">
* ...
* </csw:Record>
* }
* </pre>
*/
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
if (transformProvider == null) {
throw new ConversionException(
"Unable to locate Converter for outputSchema: " + outputSchema);
}
CswRecordCollection cswRecords = new CswRecordCollection();
List<Metacard> metacards = cswRecords.getCswRecords();
parseXmlNamespaceDeclarations(reader, context);
while (reader.hasMoreChildren()) {
reader.moveDown();
if (reader.getNodeName().contains("SearchResults")) {
setSearchResults(reader, cswRecords);
// Loop through the <SearchResults>, converting each
// <csw:Record> into a Metacard
while (reader.hasMoreChildren()) {
reader.moveDown(); // move down to the <csw:Record> tag
String name = reader.getNodeName();
LOGGER.debug("node name = {}", name);
Metacard metacard = (Metacard) context
.convertAnother(null, MetacardImpl.class, transformProvider);
metacards.add(metacard);
// move back up to the <SearchResults> parent of the
// <csw:Record> tags
reader.moveUp();
}
}
reader.moveUp();
}
LOGGER.debug("Unmarshalled {} metacards", metacards.size());
if (LOGGER.isTraceEnabled()) {
int index = 1;
for (Metacard m : metacards) {
LOGGER.trace("metacard {}: ", index);
LOGGER.trace(" id = {}", m.getId());
LOGGER.trace(" title = {}", m.getTitle());
// Some CSW services return an empty bounding box, i.e., no lower
// and/or upper corner positions
Attribute boundingBoxAttr = m.getAttribute("BoundingBox");
if (boundingBoxAttr != null) {
LOGGER.trace(" bounding box = {}", boundingBoxAttr.getValue());
}
index++;
}
}
return cswRecords;
}
private void parseXmlNamespaceDeclarations(HierarchicalStreamReader reader,
UnmarshallingContext context) {
Map<String, String> namespaces = new HashMap<>();
Iterator<String> attributeNames = reader.getAttributeNames();
while (attributeNames.hasNext()) {
String name = attributeNames.next();
if (StringUtils.startsWith(name, CswConstants.XMLNS)) {
String attributeValue = reader.getAttribute(name);
namespaces.put(name, attributeValue);
}
}
if (!namespaces.isEmpty()) {
context.put(CswConstants.WRITE_NAMESPACES, namespaces);
}
}
private void setSearchResults(HierarchicalStreamReader reader, CswRecordCollection cswRecords) {
String numberOfRecordsMatched = reader.getAttribute("numberOfRecordsMatched");
LOGGER.debug("numberOfRecordsMatched = {}", numberOfRecordsMatched);
String numberOfRecordsReturned = reader.getAttribute("numberOfRecordsReturned");
LOGGER.debug("numberOfRecordsReturned = {}", numberOfRecordsReturned);
cswRecords.setNumberOfRecordsMatched(Long.valueOf(numberOfRecordsMatched));
cswRecords.setNumberOfRecordsReturned(Long.valueOf(numberOfRecordsReturned));
}
}