org.codice.ddf.endpoints.rest.RESTEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.endpoints.rest.RESTEndpoint.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 org.codice.ddf.endpoints.rest;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.Encoded;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.apache.commons.codec.CharEncoding;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.opengis.filter.Filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.ByteSource;
import com.google.common.io.FileBackedOutputStream;

import ddf.catalog.CatalogFramework;
import ddf.catalog.Constants;
import ddf.catalog.content.data.impl.ContentItemImpl;
import ddf.catalog.content.operation.CreateStorageRequest;
import ddf.catalog.content.operation.UpdateStorageRequest;
import ddf.catalog.content.operation.impl.CreateStorageRequestImpl;
import ddf.catalog.content.operation.impl.UpdateStorageRequestImpl;
import ddf.catalog.data.BinaryContent;
import ddf.catalog.data.ContentType;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.MetacardCreationException;
import ddf.catalog.data.Result;
import ddf.catalog.data.impl.AttributeImpl;
import ddf.catalog.data.impl.BinaryContentImpl;
import ddf.catalog.federation.FederationException;
import ddf.catalog.filter.FilterBuilder;
import ddf.catalog.operation.CreateRequest;
import ddf.catalog.operation.CreateResponse;
import ddf.catalog.operation.QueryResponse;
import ddf.catalog.operation.SourceInfoResponse;
import ddf.catalog.operation.UpdateRequest;
import ddf.catalog.operation.impl.CreateRequestImpl;
import ddf.catalog.operation.impl.DeleteRequestImpl;
import ddf.catalog.operation.impl.QueryImpl;
import ddf.catalog.operation.impl.QueryRequestImpl;
import ddf.catalog.operation.impl.SourceInfoRequestEnterprise;
import ddf.catalog.operation.impl.UpdateRequestImpl;
import ddf.catalog.resource.Resource;
import ddf.catalog.source.IngestException;
import ddf.catalog.source.InternalIngestException;
import ddf.catalog.source.SourceDescriptor;
import ddf.catalog.source.SourceUnavailableException;
import ddf.catalog.source.UnsupportedQueryException;
import ddf.catalog.transform.CatalogTransformerException;
import ddf.catalog.transform.InputTransformer;
import ddf.mime.MimeTypeMapper;
import ddf.mime.MimeTypeResolutionException;
import ddf.mime.MimeTypeResolver;
import ddf.mime.MimeTypeToTransformerMapper;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import net.minidev.json.JSONValue;

@Path("/")
public class RESTEndpoint implements RESTService {

    static final String DEFAULT_METACARD_TRANSFORMER = "xml";

    static final String DEFAULT_FILE_EXTENSION = "bin";

    static final String BYTES_TO_SKIP = "BytesToSkip";

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

    private static final Logger INGEST_LOGGER = LoggerFactory.getLogger(Constants.INGEST_LOGGER_NAME);

    private static final String HEADER_RANGE = "Range";

    private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges";

    private static final String HEADER_CONTENT_LENGTH = "Content-Length";

    private static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";

    private static final String FILE_ATTACHMENT_CONTENT_ID = "file";

    private static final String FILENAME_CONTENT_DISPOSITION_PARAMETER_NAME = "filename";

    private static final String BYTES = "bytes";

    private static final String BYTES_EQUAL = "bytes=";

    private static final String JSON_MIME_TYPE_STRING = "application/json";

    static final String DEFAULT_MIME_TYPE = "application/octet-stream";

    static final String DEFAULT_FILE_NAME = "file";

    /**
     * Basic mime types that will be attempted to refine to a more accurate mime type
     * based on the file extension of the filename specified in the create request.
     */
    static final List<String> REFINEABLE_MIME_TYPES = Arrays.asList(DEFAULT_MIME_TYPE, "text/plain");

    private static MimeType jsonMimeType = null;

    private MimeTypeMapper mimeTypeMapper;

    static {
        MimeType mime = null;
        try {
            mime = new MimeType(JSON_MIME_TYPE_STRING);
        } catch (MimeTypeParseException e) {
            LOGGER.warn("Failed to create json mimetype.");
        }
        jsonMimeType = mime;

    }

    private FilterBuilder filterBuilder;

    private CatalogFramework catalogFramework;

    private MimeTypeToTransformerMapper mimeTypeToTransformerMapper;

    private MimeTypeResolver tikaMimeTypeResolver;

    public RESTEndpoint(CatalogFramework framework) {
        LOGGER.trace("Constructing REST Endpoint");
        this.catalogFramework = framework;
        LOGGER.trace(("Rest Endpoint constructed successfully"));
    }

    /**
     * REST Head. Retrieves information regarding the entry specified by the id.
     * This can be used to verify that the Range header is supported (the Accept-Ranges header is returned) and to
     * get the size of the requested resource for use in Content-Range requests.
     *
     * @param id
     * @param uriInfo
     * @param httpRequest
     * @return
     */
    @HEAD
    @Path("/{id}")
    public Response getHeaders(@PathParam("id") String id, @Context UriInfo uriInfo,
            @Context HttpServletRequest httpRequest) {

        return getHeaders(null, id, uriInfo, httpRequest);
    }

    /**
     * REST Head. Returns headers only.  Primarily used to let the client know that range requests (though limited)
     * are accepted.
     *
     * @param sourceid
     * @param id
     * @param uriInfo
     * @param httpRequest
     * @return
     */
    @HEAD
    @Path("/sources/{sourceid}/{id}")
    public Response getHeaders(@PathParam("sourceid") String sourceid, @PathParam("id") String id,
            @Context UriInfo uriInfo, @Context HttpServletRequest httpRequest) {

        Response response;
        Response.ResponseBuilder responseBuilder;
        QueryResponse queryResponse;
        Metacard card = null;

        LOGGER.trace("getHeaders");
        URI absolutePath = uriInfo.getAbsolutePath();
        MultivaluedMap<String, String> map = uriInfo.getQueryParameters();

        if (id != null) {
            LOGGER.debug("Got id: {}", id);
            LOGGER.debug("Map of query parameters: \n{}", map.toString());

            Map<String, Serializable> convertedMap = convert(map);
            convertedMap.put("url", absolutePath.toString());

            LOGGER.debug("Map converted, retrieving product.");

            // default to xml if no transformer specified
            try {
                String transformer = DEFAULT_METACARD_TRANSFORMER;

                Filter filter = getFilterBuilder().attribute(Metacard.ID).is().equalTo().text(id);

                Collection<String> sources = null;
                if (sourceid != null) {
                    sources = new ArrayList<String>();
                    sources.add(sourceid);
                }

                QueryRequestImpl request = new QueryRequestImpl(new QueryImpl(filter), sources);
                request.setProperties(convertedMap);
                queryResponse = catalogFramework.query(request, null);

                // pull the metacard out of the blocking queue
                List<Result> results = queryResponse.getResults();

                // TODO: should be poll? do we want to specify a timeout? (will
                // return null if timeout elapsed)
                if (results != null && !results.isEmpty()) {
                    card = results.get(0).getMetacard();
                }

                if (card == null) {
                    throw new ServerErrorException("Unable to retrieve requested metacard.", Status.NOT_FOUND);
                }

                LOGGER.debug("Calling transform.");
                final BinaryContent content = catalogFramework.transform(card, transformer, convertedMap);
                LOGGER.debug("Read and transform complete, preparing response.");

                responseBuilder = Response.noContent();

                // Add the Accept-ranges header to let the client know that we accept ranges in bytes
                responseBuilder.header(HEADER_ACCEPT_RANGES, BYTES);

                String filename = null;

                if (content instanceof Resource) {
                    // If we got a resource, we can extract the filename.
                    filename = ((Resource) content).getName();
                } else {
                    String fileExtension = getFileExtensionForMimeType(content.getMimeTypeValue());
                    if (StringUtils.isNotBlank(fileExtension)) {
                        filename = id + fileExtension;
                    }
                }

                if (StringUtils.isNotBlank(filename)) {
                    LOGGER.debug("filename: {}", filename);
                    responseBuilder.header(HEADER_CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
                }

                long size = content.getSize();
                if (size > 0) {
                    responseBuilder.header(HEADER_CONTENT_LENGTH, size);
                }

                response = responseBuilder.build();

            } catch (FederationException e) {
                String exceptionMessage = "READ failed due to unexpected exception: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            } catch (CatalogTransformerException e) {
                String exceptionMessage = "Unable to transform Metacard.  Try different transformer: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            } catch (SourceUnavailableException e) {
                String exceptionMessage = "Cannot obtain query results because source is unavailable: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            } catch (UnsupportedQueryException e) {
                String exceptionMessage = "Specified query is unsupported.  Change query and resubmit: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.BAD_REQUEST);

                // The catalog framework will throw this if any of the transformers blow up. We need to
                // catch this exception
                // here or else execution will return to CXF and we'll lose this message and end up with
                // a huge stack trace
                // in a GUI or whatever else is connected to this endpoint
            } catch (IllegalArgumentException e) {
                throw new ServerErrorException(e, Status.BAD_REQUEST);
            }
        } else {
            throw new ServerErrorException("No ID specified.", Status.BAD_REQUEST);
        }
        return response;
    }

    /**
     * REST Get. Retrieves the metadata entry specified by the id. Transformer argument is optional,
     * but is used to specify what format the data should be returned.
     *
     * @param id
     * @param transformerParam (OPTIONAL)
     * @param uriInfo
     * @return
     * @throws ServerErrorException
     */
    @GET
    @Path("/{id}")
    public Response getDocument(@PathParam("id") String id, @QueryParam("transform") String transformerParam,
            @Context UriInfo uriInfo, @Context HttpServletRequest httpRequest) {

        return getDocument(null, id, transformerParam, uriInfo, httpRequest);
    }

    /**
     * REST Get. Retrieves information regarding sources available.
     *
     * @param uriInfo
     * @param httpRequest
     * @return
     */
    @GET
    @Path("/sources")
    public Response getDocument(@Context UriInfo uriInfo, @Context HttpServletRequest httpRequest) {
        BinaryContent content;
        ResponseBuilder responseBuilder;
        String sourcesString = null;

        JSONArray resultsList = new JSONArray();
        SourceInfoResponse sources;
        try {
            SourceInfoRequestEnterprise sourceInfoRequestEnterprise = new SourceInfoRequestEnterprise(true);

            sources = catalogFramework.getSourceInfo(sourceInfoRequestEnterprise);
            for (SourceDescriptor source : sources.getSourceInfo()) {
                JSONObject sourceObj = new JSONObject();
                sourceObj.put("id", source.getSourceId());
                sourceObj.put("version", source.getVersion() != null ? source.getVersion() : "");
                sourceObj.put("available", Boolean.valueOf(source.isAvailable()));
                JSONArray contentTypesObj = new JSONArray();
                if (source.getContentTypes() != null) {
                    for (ContentType contentType : source.getContentTypes()) {
                        if (contentType != null && contentType.getName() != null) {
                            JSONObject contentTypeObj = new JSONObject();
                            contentTypeObj.put("name", contentType.getName());
                            contentTypeObj.put("version",
                                    contentType.getVersion() != null ? contentType.getVersion() : "");
                            contentTypesObj.add(contentTypeObj);
                        }
                    }
                }
                sourceObj.put("contentTypes", contentTypesObj);
                resultsList.add(sourceObj);
            }
        } catch (SourceUnavailableException e) {
            LOGGER.warn("Unable to retrieve Sources. {}", e.getMessage());
            LOGGER.debug("Unable to retrieve Sources", e);
        }

        sourcesString = JSONValue.toJSONString(resultsList);
        content = new BinaryContentImpl(new ByteArrayInputStream(sourcesString.getBytes(StandardCharsets.UTF_8)),
                jsonMimeType);
        responseBuilder = Response.ok(content.getInputStream(), content.getMimeTypeValue());

        // Add the Accept-ranges header to let the client know that we accept ranges in bytes
        responseBuilder.header(HEADER_ACCEPT_RANGES, BYTES);

        return responseBuilder.build();
    }

    /**
     * REST Get. Retrieves the metadata entry specified by the id from the federated source
     * specified by sourceid. Transformer argument is optional, but is used to specify what format
     * the data should be returned.
     *
     * @param sourceid
     * @param id
     * @param transformerParam
     * @param uriInfo
     * @return
     */
    @GET
    @Path("/sources/{sourceid}/{id}")
    public Response getDocument(@Encoded @PathParam("sourceid") String encodedSourceId,
            @Encoded @PathParam("id") String encodedId, @QueryParam("transform") String transformerParam,
            @Context UriInfo uriInfo, @Context HttpServletRequest httpRequest) {

        Response response = null;
        Response.ResponseBuilder responseBuilder;
        QueryResponse queryResponse;
        Metacard card = null;

        LOGGER.trace("GET");
        URI absolutePath = uriInfo.getAbsolutePath();
        MultivaluedMap<String, String> map = uriInfo.getQueryParameters();

        if (encodedId != null) {
            LOGGER.debug("Got id: {}", encodedId);
            LOGGER.debug("Got service: {}", transformerParam);
            LOGGER.debug("Map of query parameters: \n{}", map.toString());

            Map<String, Serializable> convertedMap = convert(map);
            convertedMap.put("url", absolutePath.toString());

            LOGGER.debug("Map converted, retrieving product.");

            // default to xml if no transformer specified
            try {
                String id = URLDecoder.decode(encodedId, CharEncoding.UTF_8);

                String transformer = DEFAULT_METACARD_TRANSFORMER;
                if (transformerParam != null) {
                    transformer = transformerParam;
                }
                Filter filter = getFilterBuilder().attribute(Metacard.ID).is().equalTo().text(id);

                Collection<String> sources = null;
                if (encodedSourceId != null) {
                    String sourceid = URLDecoder.decode(encodedSourceId, CharEncoding.UTF_8);
                    sources = new ArrayList<String>();
                    sources.add(sourceid);
                }

                QueryRequestImpl request = new QueryRequestImpl(new QueryImpl(filter), sources);
                request.setProperties(convertedMap);
                queryResponse = catalogFramework.query(request, null);

                // pull the metacard out of the blocking queue
                List<Result> results = queryResponse.getResults();

                // TODO: should be poll? do we want to specify a timeout? (will
                // return null if timeout elapsed)
                if (results != null && !results.isEmpty()) {
                    card = results.get(0).getMetacard();
                }

                if (card == null) {
                    throw new ServerErrorException("Unable to retrieve requested metacard.", Status.NOT_FOUND);
                }

                // Check for Range header set the value in the map appropriately so that the catalogFramework
                // can take care of the skipping
                long bytesToSkip = getRangeStart(httpRequest);

                if (bytesToSkip > 0) {
                    LOGGER.debug("Bytes to skip: {}", String.valueOf(bytesToSkip));
                    convertedMap.put(BYTES_TO_SKIP, bytesToSkip);
                }

                LOGGER.debug("Calling transform.");
                final BinaryContent content = catalogFramework.transform(card, transformer, convertedMap);
                LOGGER.debug("Read and transform complete, preparing response.");

                responseBuilder = Response.ok(content.getInputStream(), content.getMimeTypeValue());

                // Add the Accept-ranges header to let the client know that we accept ranges in bytes
                responseBuilder.header(HEADER_ACCEPT_RANGES, BYTES);

                String filename = null;

                if (content instanceof Resource) {
                    // If we got a resource, we can extract the filename.
                    filename = ((Resource) content).getName();
                } else {
                    String fileExtension = getFileExtensionForMimeType(content.getMimeTypeValue());
                    if (StringUtils.isNotBlank(fileExtension)) {
                        filename = id + fileExtension;
                    }
                }

                if (StringUtils.isNotBlank(filename)) {
                    LOGGER.debug("filename: {}", filename);
                    responseBuilder.header(HEADER_CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
                }

                long size = content.getSize();
                if (size > 0) {
                    responseBuilder.header(HEADER_CONTENT_LENGTH, size);
                }

                response = responseBuilder.build();
            } catch (FederationException e) {
                String exceptionMessage = "READ failed due to unexpected exception: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            } catch (CatalogTransformerException e) {
                String exceptionMessage = "Unable to transform Metacard.  Try different transformer: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            } catch (SourceUnavailableException e) {
                String exceptionMessage = "Cannot obtain query results because source is unavailable: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            } catch (UnsupportedQueryException e) {
                String exceptionMessage = "Specified query is unsupported.  Change query and resubmit: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.BAD_REQUEST);
                // The catalog framework will throw this if any of the transformers blow up. We need to

                // catch this exception
                // here or else execution will return to CXF and we'll lose this message and end up with
                // a huge stack trace
                // in a GUI or whatever else is connected to this endpoint
            } catch (RuntimeException | UnsupportedEncodingException e) {
                String exceptionMessage = "Unknown error occurred while processing request: ";
                LOGGER.warn(exceptionMessage, e);
                throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
            }
        } else {
            throw new ServerErrorException("No ID specified.", Status.BAD_REQUEST);
        }
        return response;
    }

    @POST
    @Path("/metacard")
    public Response createMetacard(MultipartBody multipartBody, @Context UriInfo requestUriInfo,
            @QueryParam("transform") String transformerParam) {

        LOGGER.trace("ENTERING: createMetacard");

        String contentUri = multipartBody.getAttachmentObject("contentUri", String.class);
        LOGGER.debug("contentUri = {}", contentUri);

        InputStream stream = null;
        String filename = null;
        String contentType = null;
        Response response = null;

        String transformer = DEFAULT_METACARD_TRANSFORMER;
        if (transformerParam != null) {
            transformer = transformerParam;
        }

        Attachment contentPart = multipartBody.getAttachment(FILE_ATTACHMENT_CONTENT_ID);
        if (contentPart != null) {
            // Example Content-Type header:
            // Content-Type: application/json;id=geojson
            if (contentPart.getContentType() != null) {
                contentType = contentPart.getContentType().toString();
            }

            // Get the file contents as an InputStream and ensure the stream is positioned
            // at the beginning
            try {
                stream = contentPart.getDataHandler().getInputStream();
                if (stream != null && stream.available() == 0) {
                    stream.reset();
                }
            } catch (IOException e) {
                LOGGER.warn("IOException reading stream from file attachment in multipart body", e);
            }
        } else {
            LOGGER.debug("No file contents attachment found");
        }

        MimeType mimeType = null;
        if (contentType != null) {
            try {
                mimeType = new MimeType(contentType);
            } catch (MimeTypeParseException e) {
                LOGGER.debug("Unable to create MimeType from raw data {}", contentType);
            }
        } else {
            LOGGER.debug("No content type specified in request");
        }

        try {
            Metacard metacard = generateMetacard(mimeType, "assigned-when-ingested", stream);
            String metacardId = metacard.getId();
            LOGGER.debug("Metacard {} created", metacardId);
            LOGGER.debug("Transforming metacard {} to {} to be able to return it to client", metacardId,
                    transformer);
            final BinaryContent content = catalogFramework.transform(metacard, transformer, null);
            LOGGER.debug("Metacard to {} transform complete for {}, preparing response.", transformer, metacardId);

            Response.ResponseBuilder responseBuilder = Response.ok(content.getInputStream(),
                    content.getMimeTypeValue());
            response = responseBuilder.build();
        } catch (MetacardCreationException | CatalogTransformerException e) {
            throw new ServerErrorException("Unable to create metacard", Status.BAD_REQUEST);
        }

        LOGGER.trace("EXITING: createMetacard");

        return response;
    }

    /**
     * REST Put. Updates the specified entry with the provided document.
     *
     * @param id
     * @param message
     * @return
     */
    @PUT
    @Path("/{id}")
    @Consumes({ "text/*", "application/*" })
    public Response updateDocument(@PathParam("id") String id, @Context HttpHeaders headers,
            @Context HttpServletRequest httpRequest, InputStream message) {
        return updateDocument(id, headers, httpRequest, null, message);
    }

    /**
     * REST Put. Updates the specified entry with the provided document.
     *
     * @param id
     * @param message
     * @return
     */
    @PUT
    @Path("/{id}")
    @Consumes("multipart/*")
    public Response updateDocument(@PathParam("id") String id, @Context HttpHeaders headers,
            @Context HttpServletRequest httpRequest, MultipartBody multipartBody, InputStream message) {
        LOGGER.trace("PUT");
        Response response;

        try {
            if (id != null && message != null) {

                MimeType mimeType = getMimeType(headers);

                CreateInfo createInfo = null;
                if (multipartBody != null) {
                    List<Attachment> contentParts = multipartBody.getAllAttachments();
                    if (contentParts != null && contentParts.size() > 0) {
                        createInfo = parseAttachment(contentParts.get(0));
                    } else {
                        LOGGER.debug("No file contents attachment found");
                    }
                }

                if (createInfo == null) {
                    UpdateRequest updateRequest = new UpdateRequestImpl(id,
                            generateMetacard(mimeType, id, message));
                    catalogFramework.update(updateRequest);
                } else {
                    UpdateStorageRequest streamUpdateRequest = new UpdateStorageRequestImpl(
                            Collections.singletonList(new IncomingContentItem(id, createInfo.getStream(),
                                    createInfo.getContentType(), createInfo.getFilename(), 0, null)),
                            null);
                    catalogFramework.update(streamUpdateRequest);
                }

                LOGGER.debug("Metacard {} updated.", id);
                response = Response.ok().build();
            } else {
                String errorResponseString = "Both ID and content are needed to perform UPDATE.";
                LOGGER.warn(errorResponseString);
                throw new ServerErrorException(errorResponseString, Status.BAD_REQUEST);
            }
        } catch (SourceUnavailableException e) {
            String exceptionMessage = "Cannot update catalog entry: Source is unavailable: ";
            LOGGER.warn(exceptionMessage, e);
            throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
        } catch (InternalIngestException e) {
            String exceptionMessage = "Error cataloging updated metadata: ";
            LOGGER.warn(exceptionMessage, e);
            throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
        } catch (MetacardCreationException | IngestException e) {
            String exceptionMessage = "Error cataloging updated metadata: ";
            LOGGER.warn(exceptionMessage, e);
            throw new ServerErrorException(exceptionMessage, Status.BAD_REQUEST);
        }
        return response;
    }

    @POST
    @Consumes({ "text/*", "application/*" })
    public Response addDocument(@Context HttpHeaders headers, @Context UriInfo requestUriInfo,
            @Context HttpServletRequest httpRequest, InputStream message) {
        return addDocument(headers, requestUriInfo, httpRequest, null, message);
    }

    /**
     * REST Post. Creates a new metadata entry in the catalog.
     *
     * @param message
     * @return
     */
    @POST
    @Consumes("multipart/*")
    public Response addDocument(@Context HttpHeaders headers, @Context UriInfo requestUriInfo,
            @Context HttpServletRequest httpRequest, MultipartBody multipartBody, InputStream message) {
        LOGGER.debug("POST");
        Response response;

        MimeType mimeType = getMimeType(headers);

        try {
            if (message != null) {
                CreateInfo createInfo = null;
                if (multipartBody != null) {
                    List<Attachment> contentParts = multipartBody.getAllAttachments();
                    if (contentParts != null && contentParts.size() > 0) {
                        createInfo = parseAttachment(contentParts.get(0));
                    } else {
                        LOGGER.debug("No file contents attachment found");
                    }
                }

                CreateResponse createResponse;
                if (createInfo == null) {
                    CreateRequest createRequest = new CreateRequestImpl(generateMetacard(mimeType, null, message));
                    createResponse = catalogFramework.create(createRequest);
                } else {
                    CreateStorageRequest streamCreateRequest = new CreateStorageRequestImpl(
                            Collections.singletonList(new IncomingContentItem(createInfo.getStream(),
                                    createInfo.getContentType(), createInfo.getFilename(), null)),
                            null);
                    createResponse = catalogFramework.create(streamCreateRequest);
                }

                String id = createResponse.getCreatedMetacards().get(0).getId();

                LOGGER.debug("Create Response id [{}]", id);

                UriBuilder uriBuilder = requestUriInfo.getAbsolutePathBuilder().path("/" + id);

                ResponseBuilder responseBuilder = Response.created(uriBuilder.build());

                responseBuilder.header(Metacard.ID, id);

                response = responseBuilder.build();

                LOGGER.debug("Entry successfully saved, id: {}", id);
                if (INGEST_LOGGER.isInfoEnabled()) {
                    INGEST_LOGGER.info("Entry successfully saved, id: {}", id);
                }
            } else {
                String errorMessage = "No content found, cannot do CREATE.";
                LOGGER.warn(errorMessage);
                throw new ServerErrorException(errorMessage, Status.BAD_REQUEST);
            }
        } catch (SourceUnavailableException e) {
            String exceptionMessage = "Cannot create catalog entry because source is unavailable: "
                    + e.getMessage();
            LOGGER.warn(exceptionMessage, e.getCause());
            // Catalog framework logs these exceptions to the ingest logger so we don't have to.
            throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
        } catch (InternalIngestException e) {
            String exceptionMessage = "Error while storing entry in catalog: " + e.getMessage();
            LOGGER.warn(exceptionMessage, e.getCause());
            // Catalog framework logs these exceptions to the ingest logger so we don't have to.
            throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
        } catch (MetacardCreationException | IngestException e) {
            String exceptionMessage = "Error while storing entry in catalog: " + e.getMessage();
            LOGGER.warn(exceptionMessage, e.getCause());
            // Catalog framework logs these exceptions to the ingest logger so we don't have to.
            throw new ServerErrorException(exceptionMessage, Status.BAD_REQUEST);
        } finally {
            IOUtils.closeQuietly(message);
        }

        return response;
    }

    CreateInfo parseAttachment(Attachment contentPart) {
        CreateInfo createInfo = new CreateInfo();

        InputStream stream = null;
        FileBackedOutputStream fbos = null;
        String filename = null;
        String contentType = null;

        // Get the file contents as an InputStream and ensure the stream is positioned
        // at the beginning
        try {
            stream = contentPart.getDataHandler().getInputStream();
            if (stream != null && stream.available() == 0) {
                stream.reset();
            }
            createInfo.setStream(stream);
        } catch (IOException e) {
            LOGGER.warn("IOException reading stream from file attachment in multipart body", e);
        }

        // Example Content-Type header:
        // Content-Type: application/json;id=geojson
        if (contentPart.getContentType() != null) {
            contentType = contentPart.getContentType().toString();
        }

        if (contentPart.getContentDisposition() != null) {
            filename = contentPart.getContentDisposition()
                    .getParameter(FILENAME_CONTENT_DISPOSITION_PARAMETER_NAME);
        }

        // Only interested in attachments for file uploads. Any others should be covered by
        // the FormParam arguments.
        // If the filename was not specified, then generate a default filename based on the
        // specified content type.
        if (StringUtils.isEmpty(filename)) {
            LOGGER.debug("No filename parameter provided - generating default filename");
            String fileExtension = DEFAULT_FILE_EXTENSION;
            try {
                fileExtension = mimeTypeMapper.getFileExtensionForMimeType(contentType); // DDF-2307
                if (StringUtils.isEmpty(fileExtension)) {
                    fileExtension = DEFAULT_FILE_EXTENSION;
                }
            } catch (MimeTypeResolutionException e) {
                LOGGER.debug("Exception getting file extension for contentType = {}", contentType);
            }
            filename = DEFAULT_FILE_NAME + "." + fileExtension; // DDF-2263
            LOGGER.debug("No filename parameter provided - default to {}", filename);
        } else {
            filename = FilenameUtils.getName(filename);

            // DDF-908: filename with extension was specified by the client. If the
            // contentType is null or the browser default, try to refine the contentType
            // by determining the mime type based on the filename's extension.
            if (StringUtils.isEmpty(contentType) || REFINEABLE_MIME_TYPES.contains(contentType)) {
                String fileExtension = FilenameUtils.getExtension(filename);
                LOGGER.debug("fileExtension = {}, contentType before refinement = {}", fileExtension, contentType);
                try {
                    contentType = mimeTypeMapper.getMimeTypeForFileExtension(fileExtension);
                } catch (MimeTypeResolutionException e) {
                    LOGGER.debug("Unable to refine contentType {} based on filename extension {}", contentType,
                            fileExtension);
                }
                LOGGER.debug("Refined contentType = {}", contentType);
            }
        }

        createInfo.setContentType(contentType);
        createInfo.setFilename(filename);

        return createInfo;
    }

    /**
     * REST Delete. Deletes a record from the catalog.
     *
     * @param id
     * @return
     */
    @DELETE
    @Path("/{id}")
    public Response deleteDocument(@PathParam("id") String id, @Context HttpServletRequest httpRequest) {
        LOGGER.debug("DELETE");
        Response response;
        try {
            if (id != null) {
                DeleteRequestImpl deleteReq = new DeleteRequestImpl(id);

                catalogFramework.delete(deleteReq);
                response = Response.ok(id).build();
                LOGGER.debug("Attempting to delete Metacard with id: {}", id);
            } else {
                String errorMessage = "ID of entry not specified, cannot do DELETE.";
                LOGGER.warn(errorMessage);
                throw new ServerErrorException(errorMessage, Status.BAD_REQUEST);
            }
        } catch (SourceUnavailableException ce) {
            String exceptionMessage = "Could not delete entry from catalog since the source is unavailable: ";
            LOGGER.warn(exceptionMessage, ce);
            throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
        } catch (InternalIngestException e) {
            String exceptionMessage = "Error deleting entry from catalog: ";
            LOGGER.warn(exceptionMessage, e);
            throw new ServerErrorException(exceptionMessage, Status.INTERNAL_SERVER_ERROR);
        } catch (IngestException e) {
            String exceptionMessage = "Error deleting entry from catalog: ";
            LOGGER.warn(exceptionMessage, e);
            throw new ServerErrorException(exceptionMessage, Status.BAD_REQUEST);
        }
        return response;
    }

    private Map<String, Serializable> convert(MultivaluedMap<String, String> map) {
        Map<String, Serializable> convertedMap = new HashMap<String, Serializable>();

        for (Map.Entry<String, List<String>> entry : map.entrySet()) {
            String key = entry.getKey();
            List<String> value = entry.getValue();

            if (value.size() == 1) {
                convertedMap.put(key, value.get(0));
            } else {
                // List is not serializable so we make it a String array
                convertedMap.put(key, value.toArray());
            }
        }

        return convertedMap;
    }

    private Metacard generateMetacard(MimeType mimeType, String id, InputStream message)
            throws MetacardCreationException {

        List<InputTransformer> listOfCandidates = mimeTypeToTransformerMapper.findMatches(InputTransformer.class,
                mimeType);

        LOGGER.trace("Entering generateMetacard.");
        LOGGER.debug("List of matches for mimeType [{}]: {}", mimeType, listOfCandidates);

        Metacard generatedMetacard = null;

        try (FileBackedOutputStream fileBackedOutputStream = new FileBackedOutputStream(1000000)) {

            try {
                if (null != message) {
                    IOUtils.copy(message, fileBackedOutputStream);
                } else {
                    throw new MetacardCreationException(
                            "Could not copy bytes of content message.  Message was NULL.");
                }
            } catch (IOException e) {
                throw new MetacardCreationException("Could not copy bytes of content message.", e);
            }

            Iterator<InputTransformer> it = listOfCandidates.iterator();

            StringBuilder causeMessage = new StringBuilder("Could not create metacard with mimeType ");
            causeMessage.append(mimeType);
            causeMessage.append(". Reason: ");
            while (it.hasNext()) {
                InputTransformer transformer = it.next();

                try (InputStream inputStreamMessageCopy = fileBackedOutputStream.asByteSource().openStream()) {
                    generatedMetacard = transformer.transform(inputStreamMessageCopy);
                } catch (CatalogTransformerException | IOException e) {
                    causeMessage.append(System.lineSeparator());
                    causeMessage.append(e.getMessage());
                    // The caught exception more than likely does not have the root cause message
                    // that is needed to inform the caller as to why things have failed.  Therefore
                    // we need to iterate through the chain of cause exceptions and gather up
                    // all of their message details.
                    Throwable cause = e.getCause();
                    while (null != cause && cause != cause.getCause()) {
                        causeMessage.append(System.lineSeparator());
                        causeMessage.append(cause.getMessage());
                        cause = cause.getCause();
                    }
                    LOGGER.debug("Transformer [{}] could not create metacard.", transformer, e);
                }
                if (generatedMetacard != null) {
                    break;
                }
            }

            if (generatedMetacard == null) {
                throw new MetacardCreationException(causeMessage.toString());
            }

            if (id != null) {
                generatedMetacard.setAttribute(new AttributeImpl(Metacard.ID, id));
            } else {
                LOGGER.debug("Metacard had a null id");
            }
        } catch (IOException e) {
            throw new MetacardCreationException("Could not create metacard.", e);
        }
        return generatedMetacard;

    }

    private MimeType getMimeType(HttpHeaders headers) {
        List<String> contentTypeList = headers.getRequestHeader(HttpHeaders.CONTENT_TYPE);

        String singleMimeType = null;

        if (contentTypeList != null && !contentTypeList.isEmpty()) {
            singleMimeType = contentTypeList.get(0);
            LOGGER.debug("Encountered [{}] {}", singleMimeType, HttpHeaders.CONTENT_TYPE);
        }

        MimeType mimeType = null;

        // Sending a null argument to MimeType causes NPE
        if (singleMimeType != null) {
            try {
                mimeType = new MimeType(singleMimeType);
            } catch (MimeTypeParseException e) {
                LOGGER.debug("Could not parse mime type from headers.", e);
            }
        }

        return mimeType;
    }

    private String getFileExtensionForMimeType(String mimeType) {
        String fileExtension = this.tikaMimeTypeResolver.getFileExtensionForMimeType(mimeType);
        LOGGER.debug("Mime Type [{}] resolves to file extension [{}].", mimeType, fileExtension);
        return fileExtension;
    }

    private boolean rangeHeaderExists(HttpServletRequest httpRequest) {
        boolean response = false;

        if (null != httpRequest) {
            if (null != httpRequest.getHeader(HEADER_RANGE)) {
                response = true;
            }
        }

        return response;
    }

    // Return 0 (beginning of stream) if the range header does not exist.
    private long getRangeStart(HttpServletRequest httpRequest) throws UnsupportedQueryException {
        long response = 0;

        if (httpRequest != null) {
            if (rangeHeaderExists(httpRequest)) {
                String rangeHeader = httpRequest.getHeader(HEADER_RANGE);
                String range = getRange(rangeHeader);

                if (range != null) {
                    response = Long.parseLong(range);
                }
            }
        }

        return response;
    }

    private String getRange(String rangeHeader) throws UnsupportedQueryException {
        String response = null;

        if (rangeHeader != null) {
            if (rangeHeader.startsWith(BYTES_EQUAL)) {
                String tempString = rangeHeader.substring(BYTES_EQUAL.length());
                if (tempString.contains("-")) {
                    response = rangeHeader.substring(BYTES_EQUAL.length(), rangeHeader.lastIndexOf("-"));
                } else {
                    response = rangeHeader.substring(BYTES_EQUAL.length());
                }
            } else {
                throw new UnsupportedQueryException("Invalid range header: " + rangeHeader);
            }
        }

        return response;
    }

    public MimeTypeToTransformerMapper getMimeTypeToTransformerMapper() {
        return mimeTypeToTransformerMapper;
    }

    public void setMimeTypeToTransformerMapper(MimeTypeToTransformerMapper mimeTypeToTransformerMapper) {
        this.mimeTypeToTransformerMapper = mimeTypeToTransformerMapper;
    }

    public FilterBuilder getFilterBuilder() {
        return filterBuilder;
    }

    public void setFilterBuilder(FilterBuilder filterBuilder) {
        this.filterBuilder = filterBuilder;
    }

    public void setTikaMimeTypeResolver(MimeTypeResolver mimeTypeResolver) {
        this.tikaMimeTypeResolver = mimeTypeResolver;
    }

    public void setMimeTypeMapper(MimeTypeMapper mimeTypeMapper) {
        this.mimeTypeMapper = mimeTypeMapper;
    }

    protected static class CreateInfo {
        InputStream stream = null;

        String filename = null;

        String contentType = null;

        public InputStream getStream() {
            return stream;
        }

        public void setStream(InputStream stream) {
            this.stream = stream;
        }

        public String getFilename() {
            return filename;
        }

        public void setFilename(String filename) {
            this.filename = filename;
        }

        public String getContentType() {
            return contentType;
        }

        public void setContentType(String contentType) {
            this.contentType = contentType;
        }
    }

    protected static class IncomingContentItem extends ContentItemImpl {

        private InputStream inputStream;

        public IncomingContentItem(ByteSource byteSource, String mimeTypeRawData, String filename,
                Metacard metacard) {
            super(byteSource, mimeTypeRawData, filename, metacard);
        }

        public IncomingContentItem(InputStream inputStream, String mimeTypeRawData, String filename,
                Metacard metacard) {
            super(null, mimeTypeRawData, filename, metacard);
            this.inputStream = inputStream;
        }

        public IncomingContentItem(String id, InputStream inputStream, String mimeTypeRawData, String filename,
                long size, Metacard metacard) {
            super(id, null, mimeTypeRawData, filename, size, metacard);
            this.inputStream = inputStream;
        }

        @Override
        public InputStream getInputStream() {
            return inputStream;
        }
    }
}