/** * 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.xml; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; import java.util.Date; import java.util.Map; import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; import java.util.concurrent.atomic.AtomicBoolean; import javax.activation.MimeType; import javax.activation.MimeTypeParseException; import org.apache.commons.lang.time.DateFormatUtils; import org.apache.xerces.impl.dv.util.Base64; import org.codice.ddf.parser.Parser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.copy.HierarchicalStreamCopier; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import com.thoughtworks.xstream.io.xml.XppReader; import com.thoughtworks.xstream.io.xml.xppdom.XppFactory; import ddf.catalog.data.Attribute; import ddf.catalog.data.AttributeDescriptor; import ddf.catalog.data.AttributeType; import ddf.catalog.data.AttributeType.AttributeFormat; import ddf.catalog.data.BinaryContent; import ddf.catalog.data.Metacard; import ddf.catalog.data.MetacardType; import ddf.catalog.data.Result; import ddf.catalog.data.impl.BinaryContentImpl; import ddf.catalog.operation.SourceResponse; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.transform.QueryResponseTransformer; /** * Transforms a {@link SourceResponse} object into Metacard Element XML text, which is GML 3.1.1. * compliant XML. */ public class XmlResponseQueueTransformer extends AbstractXmlTransformer implements QueryResponseTransformer { public static final int BUFFER_SIZE = 1024; /** * Writer is not thread-safe; instances should not be shared. */ // @NotThreadSafe private static class MetacardPrintWriter extends PrettyPrintWriter { private static final char[] NULL = "�".toCharArray(); private static final char[] AMP = "&".toCharArray(); private static final char[] LT = "<".toCharArray(); private static final char[] GT = ">".toCharArray(); private static final char[] CR = " ".toCharArray(); private static final char[] APOS = "'".toCharArray(); private boolean isRawText = false; public MetacardPrintWriter(Writer writer) { super(writer); } private void setRawValue(String text) { try { isRawText = true; setValue(text); } finally { isRawText = false; } } @Override protected void writeText(QuickWriter writer, String text) { if (text == null) { return; } if (isRawText) { writer.write(text); } else { int length = text.length(); for (int i = 0; i < length; i++) { char c = text.charAt(i); switch (c) { case '\0': writer.write(NULL); break; case '&': writer.write(AMP); break; case '<': writer.write(LT); break; case '>': writer.write(GT); break; case '\'': writer.write(APOS); break; case '\r': writer.write(CR); break; case '\t': case '\n': writer.write(c); break; default: if (Character.isDefined(c) && !Character.isISOControl(c)) { writer.write(c); } else { writer.write("&#x"); writer.write(Integer.toHexString(c)); writer.write(';'); } } } } } } private static class MetacardForkTask extends RecursiveTask<StringWriter> { private static final String DF_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; private final ImmutableList<Result> resultList; private final ForkJoinPool fjp; private final GeometryTransformer geometryTransformer; private final int threshold; private final AtomicBoolean cancelOperation; MetacardForkTask(ImmutableList<Result> resultList, ForkJoinPool fjp, GeometryTransformer geometryTransformer, int threshold) { this(resultList, fjp, geometryTransformer, threshold, new AtomicBoolean(false)); } private MetacardForkTask(ImmutableList<Result> resultList, ForkJoinPool fjp, GeometryTransformer geometryTransformer, int threshold, AtomicBoolean cancelOperation) { this.resultList = resultList; this.fjp = fjp; this.geometryTransformer = geometryTransformer; this.threshold = threshold; this.cancelOperation = cancelOperation; } @Override protected StringWriter compute() { if (cancelOperation.get()) { return null; } if (resultList.size() < threshold) { return doCompute(); } else { int half = resultList.size() / 2; MetacardForkTask fLeft = new MetacardForkTask(resultList.subList(0, half), fjp, geometryTransformer, threshold, cancelOperation); fLeft.fork(); MetacardForkTask fRight = new MetacardForkTask( resultList.subList(half, resultList.size()), fjp, geometryTransformer, threshold, cancelOperation); StringWriter rightList = fRight.compute(); StringWriter leftList = fLeft.join(); leftList.append(rightList.getBuffer()); return leftList; } } private StringWriter doCompute() { StringWriter stringWriter = new StringWriter(BUFFER_SIZE); MetacardPrintWriter writer = new MetacardPrintWriter(stringWriter); XmlPullParser parser; try { parser = XppFactory.createDefaultParser(); } catch (XmlPullParserException e) { throw new ConversionException("Unable to initialize pull parser.", e); } for (Result result : resultList) { Metacard metacard = result.getMetacard(); writer.startNode("metacard"); if (metacard.getId() != null) { writer.addAttribute(GML_PREFIX + ":id", metacard.getId()); } writer.startNode("type"); if (metacard.getMetacardType().getName() == null || metacard.getMetacardType().getName().length() == 0) { writer.setValue(MetacardType.DEFAULT_METACARD_TYPE_NAME); } else { writer.setValue(metacard.getMetacardType().getName()); } writer.endNode(); // type if (metacard.getSourceId() != null && metacard.getSourceId().length() > 0) { writer.startNode("source"); writer.setValue(metacard.getSourceId()); writer.endNode(); // source } Set<AttributeDescriptor> attributeDescriptors = metacard.getMetacardType() .getAttributeDescriptors(); for (AttributeDescriptor attributeDescriptor : attributeDescriptors) { String attributeName = attributeDescriptor.getName(); if (attributeName.equals("id")) { continue; } Attribute attribute = metacard.getAttribute(attributeName); if (attribute != null) { AttributeFormat format = attributeDescriptor.getType().getAttributeFormat(); try { writeAttributeToXml(writer, parser, attribute, format); } catch (CatalogTransformerException | IOException e) { cancelOperation.set(true); throw new RuntimeException("Failure to write node; operation aborted", e); } } } writer.endNode(); // metacard } writer.flush(); return stringWriter; } private void writeAttributeToXml(MetacardPrintWriter writer, XmlPullParser parser, Attribute attribute, AttributeFormat format) throws IOException, CatalogTransformerException { String attributeName = attribute.getName(); for (Serializable value : attribute.getValues()) { String xmlValue = null; switch (format) { case STRING: case BOOLEAN: case SHORT: case INTEGER: case LONG: case FLOAT: case DOUBLE: xmlValue = value.toString(); break; case DATE: Date date = (Date) value; xmlValue = DateFormatUtils.formatUTC(date, DF_PATTERN); break; case GEOMETRY: xmlValue = geoToXml(geometryTransformer.transform(attribute), parser); break; case OBJECT: ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ObjectOutput output = new ObjectOutputStream(bos)) { output.writeObject(attribute.getValue()); xmlValue = Base64.encode(bos.toByteArray()); } break; case BINARY: xmlValue = Base64.encode((byte[]) value); break; case XML: xmlValue = value.toString().replaceAll("[<][?]xml.*[?][>]", ""); break; } // Write the node if we were able to convert it. if (xmlValue != null) { // The GeometryTransformer creates an XML fragment containing // both the name - with namespaces declared - and the value if (format != AttributeFormat.GEOMETRY) { writer.startNode(TYPE_NAME_LOOKUP.get(format)); writer.addAttribute("name", attributeName); writer.startNode("value"); } if (format == AttributeFormat.XML || format == AttributeFormat.GEOMETRY) { writer.setRawValue(xmlValue); } else { writer.setValue(xmlValue); } if (format != AttributeFormat.GEOMETRY) { writer.endNode(); // value writer.endNode(); // type } } } } private String geoToXml(BinaryContent content, XmlPullParser parser) { XppReader source = new XppReader(new InputStreamReader(content.getInputStream()), parser); StringWriter stringWriter = new StringWriter(BUFFER_SIZE); PrettyPrintWriter destination = new PrettyPrintWriter(stringWriter); new HierarchicalStreamCopier().copy(source, destination); return stringWriter.toString(); } } private final ForkJoinPool fjp; private final GeometryTransformer geometryTransformer; private int threshold; private static final Logger LOGGER = LoggerFactory.getLogger(XmlResponseQueueTransformer.class); public static final MimeType MIME_TYPE = new MimeType(); /** * This lookup map is...unfortunate. The current JAXB, which will remain in use for many * contexts until/unless we refactor and rewrite all XML processing, determines the attribute * names from the metacard schema. This lookup map provides an ugly shortcut for our purposes. */ private static final Map<AttributeType.AttributeFormat, String> TYPE_NAME_LOOKUP; private static final Map<String, String> NAMESPACE_MAP; private static final String GML_PREFIX = "gml"; static { try { MIME_TYPE.setPrimaryType("text"); MIME_TYPE.setSubType("xml"); } catch (MimeTypeParseException e) { LOGGER.info("Failure creating MIME type", e); throw new ExceptionInInitializerError(e); } TYPE_NAME_LOOKUP = new ImmutableMap.Builder<AttributeType.AttributeFormat, String>() .put(AttributeFormat.BINARY, "base64Binary").put(AttributeFormat.STRING, "string") .put(AttributeFormat.BOOLEAN, "boolean").put(AttributeFormat.DATE, "dateTime") .put(AttributeFormat.DOUBLE, "double").put(AttributeFormat.SHORT, "short") .put(AttributeFormat.INTEGER, "int").put(AttributeFormat.LONG, "long") .put(AttributeFormat.FLOAT, "float").put(AttributeFormat.GEOMETRY, "geometry") .put(AttributeFormat.XML, "stringxml").put(AttributeFormat.OBJECT, "object") .build(); String nsPrefix = "xmlns"; NAMESPACE_MAP = new ImmutableMap.Builder<String, String>() .put(nsPrefix, "urn:catalog:metacard") .put(nsPrefix + ":" + GML_PREFIX, "http://www.opengis.net/gml") .put(nsPrefix + ":xlink", "http://www.w3.org/1999/xlink") .put(nsPrefix + ":smil", "http://www.w3.org/2001/SMIL20/") .put(nsPrefix + ":smillang", "http://www.w3.org/2001/SMIL20/Language").build(); } /** * Constructs a transformer that will convert query responses to XML. * The {@code ForkJoinPool} is used for splitting large collections of {@link Metacard}s * into smaller collections for concurrent processing. Currently injected through Blueprint, * if we choose to use fork-join for other tasks in the application, we should move the * construction of the pool from its current location. Conversely, if we move to Java 8 we * can simply use the new {@code commonPool} static method provided on {@code ForkJoinPool}. * * @param fjp the {@code ForkJoinPool} to inject */ public XmlResponseQueueTransformer(Parser parser, ForkJoinPool fjp) { super(parser); this.fjp = fjp; geometryTransformer = new GeometryTransformer(parser); } /** * @param threshold the fork threshold: result lists smaller than this size will be * processed serially; larger than this size will be processed in * threshold-sized chunks in parallel */ public void setThreshold(int threshold) { this.threshold = threshold <= 1 ? 2 : threshold; } @Override public BinaryContent transform(SourceResponse response, Map<String, Serializable> args) throws CatalogTransformerException { try { Writer stringWriter = new StringWriter(BUFFER_SIZE); stringWriter.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"); MetacardPrintWriter writer = new MetacardPrintWriter(stringWriter); writer.startNode("metacards"); for (Map.Entry<String, String> nsRow : NAMESPACE_MAP.entrySet()) { writer.addAttribute(nsRow.getKey(), nsRow.getValue()); } if (response.getResults() != null && !response.getResults().isEmpty()) { StringWriter metacardContent = fjp .invoke(new MetacardForkTask(ImmutableList.copyOf(response.getResults()), fjp, geometryTransformer, threshold)); writer.setRawValue(metacardContent.getBuffer().toString()); } writer.endNode(); // metacards ByteArrayInputStream bais = new ByteArrayInputStream( stringWriter.toString().getBytes()); return new BinaryContentImpl(bais, MIME_TYPE); } catch (Exception e) { LOGGER.info("Failed Query response transformation", e); throw new CatalogTransformerException("Failed Query response transformation"); } } }