ddf.catalog.transformer.response.query.atom.AtomTransformer.java Source code

Java tutorial

Introduction

Here is the source code for ddf.catalog.transformer.response.query.atom.AtomTransformer.java

Source

/**
 * 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.response.query.atom;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.xml.namespace.QName;

import org.apache.abdera.Abdera;
import org.apache.abdera.ext.geo.GeoHelper;
import org.apache.abdera.ext.geo.GeoHelper.Encoding;
import org.apache.abdera.ext.geo.Position;
import org.apache.abdera.ext.opensearch.OpenSearchConstants;
import org.apache.abdera.model.Content.Type;
import org.apache.abdera.model.Element;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;
import org.apache.abdera.model.Link;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.configuration.SystemInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;

import ddf.action.Action;
import ddf.action.ActionProvider;
import ddf.catalog.data.Attribute;
import ddf.catalog.data.AttributeDescriptor;
import ddf.catalog.data.BinaryContent;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.data.impl.BasicTypes;
import ddf.catalog.data.impl.BinaryContentImpl;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.transform.CatalogTransformerException;
import ddf.catalog.transform.MetacardTransformer;
import ddf.catalog.transform.QueryResponseTransformer;
import ddf.geo.formatter.CompositeGeometry;

/**
 * This is a {@link QueryResponseTransformer} that transforms query results into an Atom formatted
 * feed. <br>
 * Atom specification referenced and used for this implementation was found at
 * http://tools.ietf.org/html/rfc4287
 */
public class AtomTransformer implements QueryResponseTransformer {

    /**
     * This variable is a workaround. If org.apache.abdera.model.Link ever includes a "REL_PREVIEW" member variable, take this
     * variable out and replace it in any function calls with: "Link.REL_PREVIEW"
     */
    public static final String REL_PREVIEW = "preview";

    public static final MimeType MIME_TYPE = new MimeType();

    static final String DEFAULT_FEED_TITLE = "Query Response";

    static final String DEFAULT_AUTHOR = "unknown";

    static final String URN_CATALOG_ID = "urn:catalog:id:";

    static final String URN_UUID = "urn:uuid:";

    static final String DEFAULT_SOURCE_ID = "unknown";

    private static final String FEDERATION_EXTENSION_NAMESPACE = "http://a9.com/-/opensearch/extensions/federation/1.0/";

    private static final String COULD_NOT_CREATE_XML_CONTENT_MESSAGE = "Could not create xml content. Running default behavior.";

    private static final Logger LOGGER = LoggerFactory.getLogger(AtomTransformer.class);

    private static final String MIME_TYPE_JPEG = "image/jpeg";

    private static final String MIME_TYPE_OCTET_STREAM = "application/octet-stream";

    // expensive creation, meant to be done once
    private static final Abdera ABDERA = new Abdera();

    static {
        try {
            MIME_TYPE.setPrimaryType("application");
            MIME_TYPE.setSubType("atom+xml");
        } catch (MimeTypeParseException e) {
            LOGGER.info("MimeType exception during static setup", e);
            throw new ExceptionInInitializerError(e);
        }
    }

    private MetacardTransformer metacardTransformer;

    private ActionProvider viewMetacardActionProvider;

    private ActionProvider resourceActionProvider;

    private ActionProvider thumbnailActionProvider;

    private WKTReader reader = new WKTReader();

    public void setViewMetacardActionProvider(ActionProvider viewMetacardActionProvider) {
        this.viewMetacardActionProvider = viewMetacardActionProvider;
    }

    public void setResourceActionProvider(ActionProvider resourceActionProvider) {
        this.resourceActionProvider = resourceActionProvider;
    }

    public void setThumbnailActionProvider(ActionProvider thumbnailActionProvider) {
        this.thumbnailActionProvider = thumbnailActionProvider;
    }

    public void setMetacardTransformer(MetacardTransformer metacardTransformer) {
        this.metacardTransformer = metacardTransformer;
    }

    @Override
    public BinaryContent transform(SourceResponse sourceResponse, Map<String, Serializable> arguments)
            throws CatalogTransformerException {

        if (sourceResponse == null) {
            throw new CatalogTransformerException("Cannot transform null " + SourceResponse.class.getName());
        }

        Date currentDate = new Date();

        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        Feed feed = null;
        try {

            Thread.currentThread().setContextClassLoader(AtomTransformer.class.getClassLoader());

            feed = ABDERA.newFeed();

        } finally {
            Thread.currentThread().setContextClassLoader(tccl);
        }

        /*
         * Atom spec text (rfc4287) Sect 4.2.14: "The "atom:title" element is a Text construct that
         * conveys a human- readable title for an entry or feed."
         */
        feed.setTitle(DEFAULT_FEED_TITLE);

        feed.setUpdated(currentDate);

        // TODO Use the same id for the same query
        // one challenge is a query in one site should not have the same feed id
        // as a query in another site probably could factor in ddf.host and port
        // into the algorithm

        feed.setId(URN_UUID + UUID.randomUUID().toString());

        // TODO SELF LINK For the Feed, possible design --> serialize Query into
        // a URL
        /*
         * Atom spec text (rfc4287): "atom:feed elements SHOULD contain one atom:link element with a
         * rel attribute value of self. This is the preferred URI for retrieving Atom Feed Documents
         * representing this Atom feed. "
         */
        feed.addLink("#", Link.REL_SELF);

        if (!StringUtils.isEmpty(SystemInfo.getOrganization())) {

            feed.addAuthor(SystemInfo.getOrganization());
        } else {
            feed.addAuthor(DEFAULT_AUTHOR);
        }

        /*
         * Atom spec text (rfc4287 sect. 4.2.4): "The "atom:generator" element's content identifies
         * the agent used to generate a feed, for debugging and other purposes." Generator is not
         * required in the atom:feed element.
         */
        if (!StringUtils.isEmpty(SystemInfo.getSiteName())) {

            // text is required.
            feed.setGenerator(null, SystemInfo.getVersion(), SystemInfo.getSiteName());
        }

        /*
         * According to http://www.opensearch.org/Specifications/OpenSearch/1.1 specification,
         * totalResults must be a non-negative integer. Requirements: This attribute is optional.
         */
        if (sourceResponse.getHits() > -1) {
            Element hits = feed.addExtension(OpenSearchConstants.TOTAL_RESULTS);
            hits.setText(Long.toString(sourceResponse.getHits()));
        }

        if (sourceResponse.getRequest() != null && sourceResponse.getRequest().getQuery() != null) {
            Element itemsPerPage = feed.addExtension(OpenSearchConstants.ITEMS_PER_PAGE);

            Element startIndex = feed.addExtension(OpenSearchConstants.START_INDEX);

            /*
             * According to http://www.opensearch.org/Specifications/OpenSearch/1.1 specification,
             * itemsPerPage must be a non-negative integer. It is possible that Catalog pageSize is
             * set to a non-negative integer though. When non-negative we will instead we will
             * change it to the number of search results on current page.
             */
            if (sourceResponse.getRequest().getQuery().getPageSize() > -1) {
                itemsPerPage.setText(Integer.toString(sourceResponse.getRequest().getQuery().getPageSize()));
            } else {
                if (sourceResponse.getResults() != null) {
                    itemsPerPage.setText(Integer.toString(sourceResponse.getResults().size()));
                }
            }

            startIndex.setText(Integer.toString(sourceResponse.getRequest().getQuery().getStartIndex()));
        }

        for (Result result : sourceResponse.getResults()) {

            Metacard metacard = result.getMetacard();

            if (metacard == null) {
                continue;
            }

            Entry entry = feed.addEntry();

            String sourceName = DEFAULT_SOURCE_ID;

            if (result.getMetacard().getSourceId() != null) {
                sourceName = result.getMetacard().getSourceId();
            }

            Element source = entry.addExtension(new QName(FEDERATION_EXTENSION_NAMESPACE, "resultSource", "fs"));

            /*
             * According to the os-federation.xsd, the resultSource element text has a max length of
             * 16 and is the shortname of the source id. Previously, we were duplicating the names
             * in both positions, but since we truly do not have a shortname for our source ids, I
             * am purposely omitting the shortname text and leaving it as the empty string. The real
             * source id can still be found in the attribute instead.
             */

            source.setAttributeValue(new QName(FEDERATION_EXTENSION_NAMESPACE, "sourceId"), sourceName);

            if (result.getRelevanceScore() != null) {
                Element relevance = entry.addExtension(
                        new QName("http://a9.com/-/opensearch/extensions/relevance/1.0/", "score", "relevance"));
                relevance.setText(result.getRelevanceScore().toString());
            }

            entry.setId(URN_CATALOG_ID + metacard.getId());

            /*
             * Atom spec text (rfc4287): "The "atom:title" element is a Text construct that conveys
             * a human- readable title for an entry or feed."
             */
            entry.setTitle(metacard.getTitle());

            /*
             * Atom spec text (rfc4287): "The "atom:updated" element is a Date construct indicating
             * the most recent instant in time when an entry or feed was modified in a way the
             * publisher considers significant." Therefore, a new Date is used because we are making
             * the entry for the first time.
             */
            if (metacard.getModifiedDate() != null) {
                entry.setUpdated(metacard.getModifiedDate());
            } else {
                entry.setUpdated(currentDate);
            }

            /*
             * Atom spec text (rfc4287): "Typically, atom:published will be associated with the
             * initial creation or first availability of the resource."
             */
            if (metacard.getCreatedDate() != null) {
                entry.setPublished(metacard.getCreatedDate());
            }

            /*
             * For atom:link elements, Atom spec text (rfc4287): "The value "related" signifies that
             * the IRI in the value of the href attribute identifies a resource related to the
             * resource described by the containing element."
             */
            addLink(resourceActionProvider, metacard, entry, Link.REL_RELATED);

            addLink(viewMetacardActionProvider, metacard, entry, Link.REL_ALTERNATE);

            addLink(thumbnailActionProvider, metacard, entry, REL_PREVIEW);

            /*
             * Atom spec text (rfc4287) Sect. 4.2.2.: "The "atom:category" element conveys
             * information about a category associated with an entry or feed. This specification
             * assigns no meaning to the content (if any) of this element."
             */
            if (metacard.getContentTypeName() != null) {
                entry.addCategory(metacard.getContentTypeName());
            }

            for (Position position : getGeoRssPositions(metacard)) {
                GeoHelper.addPosition(entry, position, Encoding.GML);
            }

            BinaryContent binaryContent = null;

            String contentOutput = metacard.getId();
            Type atomContentType = Type.TEXT;

            if (metacardTransformer != null) {

                try {
                    binaryContent = metacardTransformer.transform(metacard, new HashMap<>());

                    // If the transformer cannot handle the data appropriately
                } catch (CatalogTransformerException e) {
                    LOGGER.warn(COULD_NOT_CREATE_XML_CONTENT_MESSAGE, e);

                    // If the transformer service is unavailable
                } catch (RuntimeException e) {
                    LOGGER.info(COULD_NOT_CREATE_XML_CONTENT_MESSAGE, e);
                }

                if (binaryContent != null) {
                    try {
                        byte[] xmlBytes = binaryContent.getByteArray();
                        if (xmlBytes != null && xmlBytes.length > 0) {
                            contentOutput = new String(xmlBytes, StandardCharsets.UTF_8);

                            atomContentType = Type.XML;
                        }
                    } catch (IOException e) {
                        LOGGER.warn(COULD_NOT_CREATE_XML_CONTENT_MESSAGE, e);
                    }
                }
            }

            tccl = Thread.currentThread().getContextClassLoader();
            try {

                Thread.currentThread().setContextClassLoader(AtomTransformer.class.getClassLoader());

                entry.setContent(contentOutput, atomContentType);

            } finally {
                Thread.currentThread().setContextClassLoader(tccl);
            }

        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try {

            tccl = Thread.currentThread().getContextClassLoader();
            try {

                Thread.currentThread().setContextClassLoader(AtomTransformer.class.getClassLoader());

                feed.writeTo(baos);

            } finally {
                Thread.currentThread().setContextClassLoader(tccl);
            }

        } catch (IOException e) {
            LOGGER.info("Could not write to output stream.", e);
            throw new CatalogTransformerException("Could not transform into Atom.", e);
        }

        return new BinaryContentImpl(new ByteArrayInputStream(baos.toByteArray()), MIME_TYPE);
    }

    // a Link object could not be made and returned without a classpath problem in the OSGi runtime
    // therefore this was a workaround that did not require me to add special logic for
    // contextclassloader
    private void addLink(ActionProvider actionProvider, Metacard metacard, Entry entry, String linkType) {

        if (actionProvider != null) {
            try {

                List<Action> actions = actionProvider.getActions(metacard);

                if (!CollectionUtils.isEmpty(actions)) {
                    if (actionProvider.equals(resourceActionProvider) && metacard.getResourceURI() != null) {

                        Link viewLink = addLinkHelper(actions.get(0), entry, linkType, MIME_TYPE_OCTET_STREAM);
                        try {
                            Long length = Long.parseLong(metacard.getResourceSize(), 10);
                            viewLink.setLength(length);
                        } catch (NumberFormatException e) {
                            LOGGER.info("Could not cast {} as Long type.", metacard.getResourceSize());
                        }

                    } else if (actionProvider.equals(thumbnailActionProvider) && metacard.getThumbnail() != null) {

                        addLinkHelper(actions.get(0), entry, linkType, MIME_TYPE_JPEG);
                    } else if (!actionProvider.equals(resourceActionProvider)
                            && !actionProvider.equals(thumbnailActionProvider)) {

                        addLinkHelper(actions.get(0), entry, linkType, MIME_TYPE_OCTET_STREAM);
                    }

                }

            } catch (RuntimeException e) {
                // ActionProvider is injected but not available
                LOGGER.debug("Could not retrieve action.", e);
            }
        }

    }

    private Link addLinkHelper(Action action, Entry entry, String linkType, String mimeType) {
        Link viewLink = entry.addLink(action.getUrl().toString(), linkType);
        viewLink.setTitle(action.getTitle());
        viewLink.setMimeType(mimeType);
        return viewLink;
    }

    private List<Position> getGeoRssPositions(Metacard metacard) {

        List<Position> georssPositions = new ArrayList<Position>();

        for (AttributeDescriptor ad : metacard.getMetacardType().getAttributeDescriptors()) {

            if (ad != null && ad.getType() != null
                    && BasicTypes.GEO_TYPE.getAttributeFormat().equals(ad.getType().getAttributeFormat())) {

                Attribute geoAttribute = metacard.getAttribute(ad.getName());

                if (geoAttribute == null) {
                    continue;
                }

                for (Serializable geo : geoAttribute.getValues()) {

                    if (geo != null) {

                        try {
                            Geometry geometry = reader.read(geo.toString());

                            CompositeGeometry formatter = CompositeGeometry.getCompositeGeometry(geometry);

                            if (null != formatter) {
                                georssPositions.addAll(formatter.toGeoRssPositions());
                            } else {
                                LOGGER.info(
                                        "When cycling through geometries, could not get composite geometry [{}]",
                                        geo);
                            }

                        } catch (ParseException e) {
                            LOGGER.info("When cycling through geometries, could not parse [{}]", geo, e);
                        }

                    }

                }
            }

        }
        return georssPositions;
    }
}