/** * 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.common.source.reader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.ext.MessageBodyReader; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswConstants; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswRecordCollection; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswSourceConfiguration; import org.codice.ddf.spatial.ogc.csw.catalog.common.transformer.TransformerManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import com.google.common.net.HttpHeaders; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.DataHolder; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.xml.XppDriver; import com.thoughtworks.xstream.io.xml.XppReader; import ddf.catalog.data.types.Core; import ddf.catalog.resource.impl.ResourceImpl; /** * Custom JAX-RS MessageBodyReader for parsing a CSW GetRecords response, extracting the search * results and CSW records. */ public class GetRecordsMessageBodyReader implements MessageBodyReader<CswRecordCollection> { private static final Logger LOGGER = LoggerFactory.getLogger(GetRecordsMessageBodyReader.class); public static final String BYTES_SKIPPED = "bytes-skipped"; private XStream xstream; private DataHolder argumentHolder; public GetRecordsMessageBodyReader(Converter converter, CswSourceConfiguration configuration) { xstream = new XStream(new XppDriver()); xstream.setClassLoader(this.getClass() .getClassLoader()); xstream.registerConverter(converter); xstream.alias(CswConstants.GET_RECORDS_RESPONSE, CswRecordCollection.class); xstream.alias(CswConstants.CSW_NAMESPACE_PREFIX + CswConstants.NAMESPACE_DELIMITER + CswConstants.GET_RECORDS_RESPONSE, CswRecordCollection.class); buildArguments(configuration); } private void buildArguments(CswSourceConfiguration configuration) { argumentHolder = xstream.newDataHolder(); argumentHolder.put(CswConstants.OUTPUT_SCHEMA_PARAMETER, configuration.getOutputSchema()); argumentHolder.put(CswConstants.CSW_MAPPING, configuration.getMetacardCswMappings()); argumentHolder.put(CswConstants.AXIS_ORDER_PROPERTY, configuration.getCswAxisOrder()); argumentHolder.put(Core.RESOURCE_URI, configuration.getMetacardMapping(Core.RESOURCE_URI)); argumentHolder.put(Core.THUMBNAIL, configuration.getMetacardMapping(Core.THUMBNAIL)); argumentHolder.put(CswConstants.TRANSFORMER_LOOKUP_KEY, TransformerManager.SCHEMA); argumentHolder.put(CswConstants.TRANSFORMER_LOOKUP_VALUE, configuration.getOutputSchema()); } @Override public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return CswRecordCollection.class.isAssignableFrom(type); } @Override public CswRecordCollection readFrom(Class<CswRecordCollection> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream inStream) throws IOException, WebApplicationException { CswRecordCollection cswRecords = null; Map<String, Serializable> resourceProperties = new HashMap<>(); // Check if the server returned a Partial Content response (hopefully in response to a range header) String contentRangeHeader = httpHeaders.getFirst(HttpHeaders.CONTENT_RANGE); if (StringUtils.isNotBlank(contentRangeHeader)) { contentRangeHeader = StringUtils.substringBetween(contentRangeHeader.toLowerCase(), "bytes ", "-"); long bytesSkipped = Long.parseLong(contentRangeHeader); resourceProperties.put(BYTES_SKIPPED, Long.valueOf(bytesSkipped)); } // If the following HTTP header exists and its value is true, the input stream will contain // raw product data String productRetrievalHeader = httpHeaders.getFirst(CswConstants.PRODUCT_RETRIEVAL_HTTP_HEADER); if (productRetrievalHeader != null && productRetrievalHeader.equalsIgnoreCase("TRUE")) { String fileName = handleContentDispositionHeader(httpHeaders); cswRecords = new CswRecordCollection(); cswRecords.setResource(new ResourceImpl(inStream, mediaType.toString(), fileName)); cswRecords.setResourceProperties(resourceProperties); return cswRecords; } // Save original input stream for any exception message that might need to be // created String originalInputStream = IOUtils.toString(inStream, "UTF-8"); LOGGER.debug("Converting to CswRecordCollection: \n {}", originalInputStream); // Re-create the input stream (since it has already been read for potential // exception message creation) inStream = new ByteArrayInputStream(originalInputStream.getBytes("UTF-8")); try { HierarchicalStreamReader reader = new XppReader(new InputStreamReader(inStream, StandardCharsets.UTF_8), XmlPullParserFactory.newInstance() .newPullParser()); cswRecords = (CswRecordCollection) xstream.unmarshal(reader, null, argumentHolder); } catch (XmlPullParserException e) { LOGGER.debug("Unable to create XmlPullParser, and cannot parse CSW Response.", e); } catch (XStreamException e) { // If an ExceptionReport is sent from the remote CSW site it will be sent with an // JAX-RS "OK" status, hence the ErrorResponse exception mapper will not fire. // Instead the ExceptionReport will come here and be treated like a GetRecords // response, resulting in an XStreamException since ExceptionReport cannot be // unmarshalled. So this catch clause is responsible for catching that XStream // exception and creating a JAX-RS response containing the original stream // (with the ExceptionReport) and rethrowing it as a WebApplicatioNException, // which CXF will wrap as a ClientException that the CswSource catches, converts // to a CswException, and logs. ByteArrayInputStream bis = new ByteArrayInputStream(originalInputStream.getBytes( StandardCharsets.UTF_8)); ResponseBuilder responseBuilder = Response.ok(bis); responseBuilder.type("text/xml"); Response response = responseBuilder.build(); throw new WebApplicationException(e, response); } finally { IOUtils.closeQuietly(inStream); } return cswRecords; } /** * Check Content-Disposition header for filename and return it * * @param httpHeaders The HTTP headers * @return the filename */ private String handleContentDispositionHeader(MultivaluedMap<String, String> httpHeaders) { String contentDispositionHeader = httpHeaders.getFirst(HttpHeaders.CONTENT_DISPOSITION); if (StringUtils.isNotBlank(contentDispositionHeader)) { ContentDisposition contentDisposition = new ContentDisposition(contentDispositionHeader); String filename = contentDisposition.getParameter("filename"); if (StringUtils.isNotBlank(filename)) { LOGGER.debug("Found Content-Disposition header, changing resource name to {}", filename); return filename; } } return ""; } }