ca.uhn.fhir.rest.server.RestfulServerUtils.java Source code

Java tutorial

Introduction

Here is the source code for ca.uhn.fhir.rest.server.RestfulServerUtils.java

Source

package ca.uhn.fhir.rest.server;

/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2016 University Health Network
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.Tag;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.client.apache.ApacheHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.method.ElementsParameter;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.method.SummaryEnumParameter;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.DateUtils;

public class RestfulServerUtils {
    static final Pattern ACCEPT_HEADER_PATTERN = Pattern
            .compile("\\s*([a-zA-Z0-9+.*/-]+)\\s*(;\\s*([a-zA-Z]+)\\s*=\\s*([a-zA-Z0-9.]+)\\s*)?(,?)");

    private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class);

    private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<String>(
            Arrays.asList("Bundle", "*.text", "*.(mandatory)"));

    public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
        // Pretty print
        boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(),
                theRequestDetails);

        parser.setPrettyPrint(prettyPrint);
        parser.setServerBaseUrl(theRequestDetails.getFhirServerBase());

        // Summary mode
        Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequestDetails);

        // _elements
        Set<String> elements = ElementsParameter.getElementsValueOrNull(theRequestDetails);
        if (elements != null && summaryMode != null
                && !summaryMode.equals(Collections.singleton(SummaryEnum.FALSE))) {
            throw new InvalidRequestException("Cannot combine the " + Constants.PARAM_SUMMARY + " and "
                    + Constants.PARAM_ELEMENTS + " parameters");
        }
        Set<String> elementsAppliesTo = null;
        if (elements != null && isNotBlank(theRequestDetails.getResourceName())) {
            elementsAppliesTo = Collections.singleton(theRequestDetails.getResourceName());
        }

        if (summaryMode != null) {
            if (summaryMode.contains(SummaryEnum.COUNT)) {
                parser.setEncodeElements(Collections.singleton("Bundle.total"));
            } else if (summaryMode.contains(SummaryEnum.TEXT)) {
                parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
            } else {
                parser.setSuppressNarratives(summaryMode.contains(SummaryEnum.DATA));
                parser.setSummaryMode(summaryMode.contains(SummaryEnum.TRUE));
            }
        }
        if (elements != null && elements.size() > 0) {
            Set<String> newElements = new HashSet<String>();
            for (String next : elements) {
                newElements.add("*." + next);
            }
            parser.setEncodeElements(newElements);
            parser.setEncodeElementsAppliesToResourceTypes(elementsAppliesTo);
        }
    }

    public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId,
            int theOffset, int theCount, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
            BundleTypeEnum theBundleType) {
        try {
            StringBuilder b = new StringBuilder();
            b.append(theServerBase);
            b.append('?');
            b.append(Constants.PARAM_PAGINGACTION);
            b.append('=');
            b.append(URLEncoder.encode(theSearchId, "UTF-8"));

            b.append('&');
            b.append(Constants.PARAM_PAGINGOFFSET);
            b.append('=');
            b.append(theOffset);
            b.append('&');
            b.append(Constants.PARAM_COUNT);
            b.append('=');
            b.append(theCount);
            if (theResponseEncoding != null) {
                b.append('&');
                b.append(Constants.PARAM_FORMAT);
                b.append('=');
                b.append(theResponseEncoding.getRequestContentType());
            }
            if (thePrettyPrint) {
                b.append('&');
                b.append(Constants.PARAM_PRETTY);
                b.append('=');
                b.append(Constants.PARAM_PRETTY_VALUE_TRUE);
            }

            if (theIncludes != null) {
                for (Include nextInclude : theIncludes) {
                    if (isNotBlank(nextInclude.getValue())) {
                        b.append('&');
                        b.append(Constants.PARAM_INCLUDE);
                        b.append('=');
                        b.append(URLEncoder.encode(nextInclude.getValue(), "UTF-8"));
                    }
                }
            }

            if (theBundleType != null) {
                b.append('&');
                b.append(Constants.PARAM_BUNDLETYPE);
                b.append('=');
                b.append(theBundleType.getCode());
            }

            return b.toString();
        } catch (UnsupportedEncodingException e) {
            throw new Error("UTF-8 not supported", e);// should not happen
        }
    }

    /**
     * @TODO: this method is only called from one place and should be removed anyway
     */
    public static EncodingEnum determineRequestEncoding(RequestDetails theReq) {
        EncodingEnum retVal = determineRequestEncodingNoDefault(theReq);
        if (retVal != null) {
            return retVal;
        }
        return EncodingEnum.XML;
    }

    public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq) {
        ResponseEncoding retVal = determineRequestEncodingNoDefaultReturnRE(theReq);
        if (retVal == null) {
            return null;
        }
        return retVal.getEncoding();
    }

    private static ResponseEncoding determineRequestEncodingNoDefaultReturnRE(RequestDetails theReq) {
        ResponseEncoding retVal = null;
        List<String> headers = theReq.getHeaders(Constants.HEADER_CONTENT_TYPE);
        if (headers != null) {
            Iterator<String> acceptValues = headers.iterator();
            if (acceptValues != null) {
                while (acceptValues.hasNext() && retVal == null) {
                    String nextAcceptHeaderValue = acceptValues.next();
                    if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) {
                        for (String nextPart : nextAcceptHeaderValue.split(",")) {
                            int scIdx = nextPart.indexOf(';');
                            if (scIdx == 0) {
                                continue;
                            }
                            if (scIdx != -1) {
                                nextPart = nextPart.substring(0, scIdx);
                            }
                            nextPart = nextPart.trim();
                            EncodingEnum encoding = EncodingEnum.forContentType(nextPart);
                            if (encoding != null) {
                                retVal = new ResponseEncoding(theReq.getServer().getFhirContext(), encoding,
                                        nextPart);
                                break;
                            }
                        }
                    }
                }
            }
        }
        return retVal;
    }

    /**
     * Returns null if the request doesn't express that it wants FHIR. If it expresses that it wants XML and JSON
     * equally, returns thePrefer.
     */
    public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq,
            EncodingEnum thePrefer) {
        String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT);
        if (format != null) {
            for (String nextFormat : format) {
                EncodingEnum retVal = EncodingEnum.forContentType(nextFormat);
                if (retVal != null) {
                    return new ResponseEncoding(theReq.getServer().getFhirContext(), retVal, nextFormat);
                }
            }
        }

        /*
         * Some browsers (e.g. FF) request "application/xml" in their Accept header,
         * and we generally want to treat this as a preference for FHIR XML even if
         * it's not the FHIR version of the CT, which should be "application/xml+fhir".
         * 
         * When we're serving up Binary resources though, we are a bit more strict,
         * since Binary is supposed to use native content types unless the client has
         * explicitly requested FHIR.
         */
        boolean strict = false;
        if ("Binary".equals(theReq.getResourceName())) {
            strict = true;
        }

        /*
         * The Accept header is kind of ridiculous, e.g.
         */
        // text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8, image/png, */*;q=0.5

        List<String> acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT);
        float bestQ = -1f;
        ResponseEncoding retVal = null;
        if (acceptValues != null) {
            for (String nextAcceptHeaderValue : acceptValues) {
                StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, ",");
                while (tok.hasMoreTokens()) {
                    String nextToken = tok.nextToken();
                    int startSpaceIndex = -1;
                    for (int i = 0; i < nextToken.length(); i++) {
                        if (nextToken.charAt(i) != ' ') {
                            startSpaceIndex = i;
                            break;
                        }
                    }

                    if (startSpaceIndex == -1) {
                        continue;
                    }

                    int endSpaceIndex = -1;
                    for (int i = startSpaceIndex; i < nextToken.length(); i++) {
                        if (nextToken.charAt(i) == ' ' || nextToken.charAt(i) == ';') {
                            endSpaceIndex = i;
                            break;
                        }
                    }

                    float q = 1.0f;
                    ResponseEncoding encoding;
                    if (endSpaceIndex == -1) {
                        if (startSpaceIndex == 0) {
                            encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict,
                                    nextToken);
                        } else {
                            encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict,
                                    nextToken.substring(startSpaceIndex));
                        }
                    } else {
                        encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict,
                                nextToken.substring(startSpaceIndex, endSpaceIndex));
                        String remaining = nextToken.substring(endSpaceIndex + 1);
                        StringTokenizer qualifierTok = new StringTokenizer(remaining, ";");
                        while (qualifierTok.hasMoreTokens()) {
                            String nextQualifier = qualifierTok.nextToken();
                            int equalsIndex = nextQualifier.indexOf('=');
                            if (equalsIndex != -1) {
                                String nextQualifierKey = nextQualifier.substring(0, equalsIndex).trim();
                                String nextQualifierValue = nextQualifier
                                        .substring(equalsIndex + 1, nextQualifier.length()).trim();
                                if (nextQualifierKey.equals("q")) {
                                    try {
                                        q = Float.parseFloat(nextQualifierValue);
                                        q = Math.max(q, 0.0f);
                                    } catch (NumberFormatException e) {
                                        ourLog.debug("Invalid Accept header q value: {}", nextQualifierValue);
                                    }
                                }
                            }
                        }
                    }

                    if (encoding != null) {
                        if (q > bestQ || (q == bestQ && encoding.getEncoding() == thePrefer)) {
                            retVal = encoding;
                            bestQ = q;
                        }
                    }

                }

            }

        }

        /*
         * If the client hasn't given any indication about which response
         * encoding they want, let's try the request encoding in case that
         * is useful (basically this catches the case where the request
         * has a Content-Type header but not an Accept header)
         */
        if (retVal == null) {
            retVal = determineRequestEncodingNoDefaultReturnRE(theReq);
        }

        return retVal;
    }

    /**
     * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's
     * <code>"_format"</code> parameter and <code>"Accept:"</code> HTTP header.
     */
    public static ResponseEncoding determineResponseEncodingWithDefault(RequestDetails theReq) {
        ResponseEncoding retVal = determineResponseEncodingNoDefault(theReq,
                theReq.getServer().getDefaultResponseEncoding());
        if (retVal == null) {
            retVal = new ResponseEncoding(theReq.getServer().getFhirContext(),
                    theReq.getServer().getDefaultResponseEncoding(), null);
        }
        return retVal;
    }

    public static Set<SummaryEnum> determineSummaryMode(RequestDetails theRequest) {
        Map<String, String[]> requestParams = theRequest.getParameters();

        Set<SummaryEnum> retVal = SummaryEnumParameter.getSummaryValueOrNull(theRequest);

        if (retVal == null) {
            /*
             * HAPI originally supported a custom parameter called _narrative, but this has been superceded by an official
             * parameter called _summary
             */
            String[] narrative = requestParams.get(Constants.PARAM_NARRATIVE);
            if (narrative != null && narrative.length > 0) {
                try {
                    NarrativeModeEnum narrativeMode = NarrativeModeEnum.valueOfCaseInsensitive(narrative[0]);
                    switch (narrativeMode) {
                    case NORMAL:
                        retVal = Collections.singleton(SummaryEnum.FALSE);
                        break;
                    case ONLY:
                        retVal = Collections.singleton(SummaryEnum.TEXT);
                        break;
                    case SUPPRESS:
                        retVal = Collections.singleton(SummaryEnum.DATA);
                        break;
                    }
                } catch (IllegalArgumentException e) {
                    ourLog.debug("Invalid {} parameger: {}", Constants.PARAM_NARRATIVE, narrative[0]);
                }
            }
        }
        if (retVal == null) {
            retVal = Collections.singleton(SummaryEnum.FALSE);
        }

        return retVal;
    }

    public static Integer extractCountParameter(RequestDetails theRequest) {
        return RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_COUNT);
    }

    public static IPrimitiveType<Date> extractLastUpdatedFromResource(IBaseResource theResource) {
        IPrimitiveType<Date> lastUpdated = null;
        if (theResource instanceof IResource) {
            lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) theResource);
        } else if (theResource instanceof IAnyResource) {
            lastUpdated = new InstantDt(((IAnyResource) theResource).getMeta().getLastUpdated());
        }
        return lastUpdated;
    }

    public static IIdType fullyQualifyResourceIdOrReturnNull(IRestfulServerDefaults theServer,
            IBaseResource theResource, String theServerBase, IIdType theResourceId) {
        IIdType retVal = null;
        if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) {
            String resName = theResourceId.getResourceType();
            if (theResource != null && isBlank(resName)) {
                resName = theServer.getFhirContext().getResourceDefinition(theResource).getName();
            }
            if (isNotBlank(resName)) {
                retVal = theResourceId.withServerBase(theServerBase, resName);
            }
        }
        return retVal;
    }

    private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict,
            String theContentType) {
        EncodingEnum encoding;
        if (theStrict) {
            encoding = EncodingEnum.forContentTypeStrict(theContentType);
        } else {
            encoding = EncodingEnum.forContentType(theContentType);
        }
        if (encoding == null) {
            return null;
        } else {
            return new ResponseEncoding(theFhirContext, encoding, theContentType);
        }
    }

    public static IParser getNewParser(FhirContext theContext, RequestDetails theRequestDetails) {

        // Determine response encoding
        EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails)
                .getEncoding();
        IParser parser;
        switch (responseEncoding) {
        case JSON:
            parser = theContext.newJsonParser();
            break;
        case XML:
        default:
            parser = theContext.newXmlParser();
            break;
        }

        configureResponseParser(theRequestDetails, parser);

        return parser;
    }

    public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) {
        Set<String> retVal = new HashSet<String>();

        Enumeration<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT);
        if (acceptValues != null) {
            float bestQ = -1f;
            while (acceptValues.hasMoreElements()) {
                String nextAcceptHeaderValue = acceptValues.nextElement();
                Matcher m = ACCEPT_HEADER_PATTERN.matcher(nextAcceptHeaderValue);
                float q = 1.0f;
                while (m.find()) {
                    String contentTypeGroup = m.group(1);
                    if (isNotBlank(contentTypeGroup)) {

                        String name = m.group(3);
                        String value = m.group(4);
                        if (name != null && value != null) {
                            if ("q".equals(name)) {
                                try {
                                    q = Float.parseFloat(value);
                                    q = Math.max(q, 0.0f);
                                } catch (NumberFormatException e) {
                                    ourLog.debug("Invalid Accept header q value: {}", value);
                                }
                            }
                        }

                        if (q > bestQ) {
                            retVal.clear();
                            bestQ = q;
                        }

                        if (q == bestQ) {
                            retVal.add(contentTypeGroup.trim());
                        }

                    }

                    if (!",".equals(m.group(5))) {
                        break;
                    }
                }

            }
        }

        return retVal;
    }

    public static PreferReturnEnum parsePreferHeader(String theValue) {
        if (isBlank(theValue)) {
            return null;
        }

        StringTokenizer tok = new StringTokenizer(theValue, ",");
        while (tok.hasMoreTokens()) {
            String next = tok.nextToken();
            int eqIndex = next.indexOf('=');
            if (eqIndex == -1 || eqIndex >= next.length() - 2) {
                continue;
            }

            String key = next.substring(0, eqIndex).trim();
            if (key.equals(Constants.HEADER_PREFER_RETURN) == false) {
                continue;
            }

            String value = next.substring(eqIndex + 1).trim();
            if (value.length() < 2) {
                continue;
            }
            if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
                value = value.substring(1, value.length() - 1);
            }

            return PreferReturnEnum.fromHeaderValue(value);
        }

        return null;
    }

    public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) {
        Map<String, String[]> requestParams = theRequest.getParameters();
        String[] pretty = requestParams.get(Constants.PARAM_PRETTY);
        boolean prettyPrint;
        if (pretty != null && pretty.length > 0) {
            if (Constants.PARAM_PRETTY_VALUE_TRUE.equals(pretty[0])) {
                prettyPrint = true;
            } else {
                prettyPrint = false;
            }
        } else {
            prettyPrint = theServer.isDefaultPrettyPrint();
            List<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT);
            if (acceptValues != null) {
                for (String nextAcceptHeaderValue : acceptValues) {
                    if (nextAcceptHeaderValue.contains("pretty=true")) {
                        prettyPrint = true;
                    }
                }
            }
        }
        return prettyPrint;
    }

    public static Object streamResponseAsBundle(IRestfulServerDefaults theServer, Bundle bundle,
            Set<SummaryEnum> theSummaryMode, boolean respondGzip, RequestDetails theRequestDetails)
            throws IOException {

        int status = 200;

        // Determine response encoding
        EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails)
                .getEncoding();

        String contentType = responseEncoding.getBundleContentType();

        String charset = Constants.CHARSET_NAME_UTF8;
        Writer writer = theRequestDetails.getResponse().getResponseWriter(status, null, contentType, charset,
                respondGzip);

        try {
            IParser parser = RestfulServerUtils.getNewParser(theServer.getFhirContext(), theRequestDetails);
            if (theSummaryMode.contains(SummaryEnum.TEXT)) {
                parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
            }
            parser.encodeBundleToWriter(bundle, writer);
        } catch (Exception e) {
            // always send a response, even if the parsing went wrong
        }
        return theRequestDetails.getResponse().sendWriterResponse(status, contentType, charset, writer);
    }

    public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource,
            Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader,
            boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
        return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null,
                theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
    }

    public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource,
            Set<SummaryEnum> theSummaryMode, int theStausCode, String theStatusMessage,
            boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails,
            IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
            throws IOException {
        IRestfulResponse restUtil = theRequestDetails.getResponse();

        // Determine response encoding
        ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails,
                theServer.getDefaultResponseEncoding());

        String serverBase = theRequestDetails.getFhirServerBase();
        IIdType fullId = null;
        if (theOperationResourceId != null) {
            fullId = theOperationResourceId;
        } else if (theResource != null) {
            if (theResource.getIdElement() != null) {
                IIdType resourceId = theResource.getIdElement();
                fullId = fullyQualifyResourceIdOrReturnNull(theServer, theResource, serverBase, resourceId);
            }
        }

        if (theAddContentLocationHeader && fullId != null) {
            if (theServer.getFhirContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
                restUtil.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue());
            }
            restUtil.addHeader(Constants.HEADER_LOCATION, fullId.getValue());
        }

        if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) {
            if (fullId != null && fullId.hasVersionIdPart()) {
                restUtil.addHeader(Constants.HEADER_ETAG, "W/\"" + fullId.getVersionIdPart() + '"');
            }
        }

        String contentType;
        if (theResource instanceof IBaseBinary && responseEncoding == null) {
            IBaseBinary bin = (IBaseBinary) theResource;
            if (isNotBlank(bin.getContentType())) {
                contentType = bin.getContentType();
            } else {
                contentType = Constants.CT_OCTET_STREAM;
            }
            // Force binary resources to download - This is a security measure to prevent
            // malicious images or HTML blocks being served up as content.
            restUtil.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");

            return restUtil.sendAttachmentResponse(bin, theStausCode, contentType);
        }

        // Ok, we're not serving a binary resource, so apply default encoding
        if (responseEncoding == null) {
            responseEncoding = new ResponseEncoding(theServer.getFhirContext(),
                    theServer.getDefaultResponseEncoding(), null);
        }

        boolean encodingDomainResourceAsText = theSummaryMode.contains(SummaryEnum.TEXT);
        if (encodingDomainResourceAsText) {
            /*
             * If the user requests "text" for a bundle, only suppress the non text elements in the Element.entry.resource
             * parts, we're not streaming just the narrative as HTML (since bundles don't even
             * have one)
             */
            if ("Bundle".equals(theServer.getFhirContext().getResourceDefinition(theResource).getName())) {
                encodingDomainResourceAsText = false;
            }
        }

        /*
         * Last-Modified header
         */

        IPrimitiveType<Date> lastUpdated;
        if (theOperationResourceLastUpdated != null) {
            lastUpdated = theOperationResourceLastUpdated;
        } else {
            lastUpdated = extractLastUpdatedFromResource(theResource);
        }
        if (lastUpdated != null && lastUpdated.isEmpty() == false) {
            restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue()));
        }

        /*
         * Category header (DSTU1 only)
         */

        if (theResource instanceof IResource
                && theServer.getFhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) {
            TagList list = (TagList) ((IResource) theResource).getResourceMetadata()
                    .get(ResourceMetadataKeyEnum.TAG_LIST);
            if (list != null) {
                for (Tag tag : list) {
                    if (StringUtils.isNotBlank(tag.getTerm())) {
                        restUtil.addHeader(Constants.HEADER_CATEGORY, tag.toHeaderValue());
                    }
                }
            }
        }

        /*
         * Stream the response body
         */

        if (theResource == null) {
            contentType = null;
        } else if (encodingDomainResourceAsText) {
            contentType = Constants.CT_HTML;
        } else {
            contentType = responseEncoding.getResourceContentType();
        }
        String charset = Constants.CHARSET_NAME_UTF8;

        Writer writer = restUtil.getResponseWriter(theStausCode, theStatusMessage, contentType, charset,
                respondGzip);
        if (theResource == null) {
            // No response is being returned
        } else if (encodingDomainResourceAsText && theResource instanceof IResource) {
            writer.append(((IResource) theResource).getText().getDiv().getValueAsString());
        } else {
            IParser parser = getNewParser(theServer.getFhirContext(), theRequestDetails);
            parser.encodeResourceToWriter(theResource, writer);
        }

        return restUtil.sendWriterResponse(theStausCode, contentType, charset, writer);
    }

    public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) {
        String[] retVal = theRequest.getParameters().get(theParamName);
        if (retVal == null) {
            return null;
        }
        try {
            return Integer.parseInt(retVal[0]);
        } catch (NumberFormatException e) {
            ourLog.debug("Failed to parse {} value '{}': {}", new Object[] { theParamName, retVal[0], e });
            return null;
        }
    }

    // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) {
    // String countString = theRequest.getParameter(name);
    // Integer count = null;
    // if (isNotBlank(countString)) {
    // try {
    // count = Integer.parseInt(countString);
    // } catch (NumberFormatException e) {
    // ourLog.debug("Failed to parse _count value '{}': {}", countString, e);
    // }
    // }
    // return count;
    // }

    public static void validateResourceListNotNull(List<? extends IBaseResource> theResourceList) {
        if (theResourceList == null) {
            throw new InternalErrorException(
                    "IBundleProvider returned a null list of resources - This is not allowed");
        }
    }

    private static enum NarrativeModeEnum {
        NORMAL, ONLY, SUPPRESS;

        public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) {
            return valueOf(NarrativeModeEnum.class, theCode.toUpperCase());
        }
    }

    /**
     * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)}
     */
    public static class ResponseEncoding {
        private final EncodingEnum myEncoding;
        private final Boolean myNonLegacy;

        public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) {
            super();
            myEncoding = theEncoding;
            if (theContentType != null) {
                if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING)
                        || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) {
                    myNonLegacy = !theCtx.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3);
                } else {
                    myNonLegacy = EncodingEnum.isNonLegacy(theContentType);
                }
            } else {
                if (theCtx.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
                    myNonLegacy = null;
                } else {
                    myNonLegacy = Boolean.TRUE;
                }
            }
        }

        public EncodingEnum getEncoding() {
            return myEncoding;
        }

        public String getResourceContentType() {
            if (Boolean.TRUE.equals(isNonLegacy())) {
                return getEncoding().getResourceContentTypeNonLegacy();
            } else {
                return getEncoding().getResourceContentType();
            }
        }

        public Boolean isNonLegacy() {
            return myNonLegacy;
        }
    }

    public static void addAcceptHeaderToRequest(EncodingEnum theEncoding, IHttpRequest theHttpRequest,
            FhirContext theContext) {
        if (theEncoding == null) {
            if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2) == false) {
                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_LEGACY);
            } else {
                theHttpRequest.addHeader(Constants.HEADER_ACCEPT,
                        Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY);
            }
        } else if (theEncoding == EncodingEnum.JSON) {
            if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2) == false) {
                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON);
            } else {
                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY);
            }
        } else if (theEncoding == EncodingEnum.XML) {
            if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2) == false) {
                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_XML);
            } else {
                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY);
            }
        }

    }

}