001package ca.uhn.fhir.rest.server;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 * http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022import static org.apache.commons.lang3.StringUtils.isBlank;
023import static org.apache.commons.lang3.StringUtils.isNotBlank;
024
025import java.io.IOException;
026import java.io.UnsupportedEncodingException;
027import java.io.Writer;
028import java.net.URLEncoder;
029import java.util.Arrays;
030import java.util.Collections;
031import java.util.Date;
032import java.util.Enumeration;
033import java.util.HashSet;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.StringTokenizer;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042import javax.servlet.http.HttpServletRequest;
043
044import org.apache.commons.lang3.StringUtils;
045import org.hl7.fhir.instance.model.api.IAnyResource;
046import org.hl7.fhir.instance.model.api.IBaseBinary;
047import org.hl7.fhir.instance.model.api.IBaseResource;
048import org.hl7.fhir.instance.model.api.IIdType;
049import org.hl7.fhir.instance.model.api.IPrimitiveType;
050
051import ca.uhn.fhir.context.FhirContext;
052import ca.uhn.fhir.context.FhirVersionEnum;
053import ca.uhn.fhir.model.api.Bundle;
054import ca.uhn.fhir.model.api.IResource;
055import ca.uhn.fhir.model.api.Include;
056import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
057import ca.uhn.fhir.model.api.Tag;
058import ca.uhn.fhir.model.api.TagList;
059import ca.uhn.fhir.model.primitive.InstantDt;
060import ca.uhn.fhir.model.valueset.BundleTypeEnum;
061import ca.uhn.fhir.parser.IParser;
062import ca.uhn.fhir.rest.api.PreferReturnEnum;
063import ca.uhn.fhir.rest.api.SummaryEnum;
064import ca.uhn.fhir.rest.client.api.IHttpRequest;
065import ca.uhn.fhir.rest.method.ElementsParameter;
066import ca.uhn.fhir.rest.method.RequestDetails;
067import ca.uhn.fhir.rest.method.SummaryEnumParameter;
068import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
069import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
070import ca.uhn.fhir.util.DateUtils;
071
072public class RestfulServerUtils {
073        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*)?(,?)");
074
075        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class);
076
077        private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<String>(Arrays.asList("Bundle", "*.text", "*.(mandatory)"));
078
079        public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
080                // Pretty print
081                boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails);
082
083                parser.setPrettyPrint(prettyPrint);
084                parser.setServerBaseUrl(theRequestDetails.getFhirServerBase());
085
086                // Summary mode
087                Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequestDetails);
088
089                // _elements
090                Set<String> elements = ElementsParameter.getElementsValueOrNull(theRequestDetails);
091                if (elements != null && summaryMode != null && !summaryMode.equals(Collections.singleton(SummaryEnum.FALSE))) {
092                        throw new InvalidRequestException("Cannot combine the " + Constants.PARAM_SUMMARY + " and " + Constants.PARAM_ELEMENTS + " parameters");
093                }
094                Set<String> elementsAppliesTo = null;
095                if (elements != null && isNotBlank(theRequestDetails.getResourceName())) {
096                        elementsAppliesTo = Collections.singleton(theRequestDetails.getResourceName());
097                }
098
099                if (summaryMode != null) {
100                        if (summaryMode.contains(SummaryEnum.COUNT)) {
101                                parser.setEncodeElements(Collections.singleton("Bundle.total"));
102                        } else if (summaryMode.contains(SummaryEnum.TEXT)) {
103                                parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
104                        } else {
105                                parser.setSuppressNarratives(summaryMode.contains(SummaryEnum.DATA));
106                                parser.setSummaryMode(summaryMode.contains(SummaryEnum.TRUE));
107                        }
108                }
109                if (elements != null && elements.size() > 0) {
110                        Set<String> newElements = new HashSet<String>();
111                        for (String next : elements) {
112                                newElements.add("*." + next);
113                        }
114                        parser.setEncodeElements(newElements);
115                        parser.setEncodeElementsAppliesToResourceTypes(elementsAppliesTo);
116                }
117        }
118
119        public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
120                        BundleTypeEnum theBundleType) {
121                try {
122                        StringBuilder b = new StringBuilder();
123                        b.append(theServerBase);
124                        b.append('?');
125                        b.append(Constants.PARAM_PAGINGACTION);
126                        b.append('=');
127                        b.append(URLEncoder.encode(theSearchId, "UTF-8"));
128
129                        b.append('&');
130                        b.append(Constants.PARAM_PAGINGOFFSET);
131                        b.append('=');
132                        b.append(theOffset);
133                        b.append('&');
134                        b.append(Constants.PARAM_COUNT);
135                        b.append('=');
136                        b.append(theCount);
137                        if (theResponseEncoding != null) {
138                                b.append('&');
139                                b.append(Constants.PARAM_FORMAT);
140                                b.append('=');
141                                b.append(theResponseEncoding.getRequestContentType());
142                        }
143                        if (thePrettyPrint) {
144                                b.append('&');
145                                b.append(Constants.PARAM_PRETTY);
146                                b.append('=');
147                                b.append(Constants.PARAM_PRETTY_VALUE_TRUE);
148                        }
149
150                        if (theIncludes != null) {
151                                for (Include nextInclude : theIncludes) {
152                                        if (isNotBlank(nextInclude.getValue())) {
153                                                b.append('&');
154                                                b.append(Constants.PARAM_INCLUDE);
155                                                b.append('=');
156                                                b.append(URLEncoder.encode(nextInclude.getValue(), "UTF-8"));
157                                        }
158                                }
159                        }
160
161                        if (theBundleType != null) {
162                                b.append('&');
163                                b.append(Constants.PARAM_BUNDLETYPE);
164                                b.append('=');
165                                b.append(theBundleType.getCode());
166                        }
167
168                        return b.toString();
169                } catch (UnsupportedEncodingException e) {
170                        throw new Error("UTF-8 not supported", e);// should not happen
171                }
172        }
173
174        /**
175         * @TODO: this method is only called from one place and should be removed anyway
176         */
177        public static EncodingEnum determineRequestEncoding(RequestDetails theReq) {
178                EncodingEnum retVal = determineRequestEncodingNoDefault(theReq);
179                if (retVal != null) {
180                        return retVal;
181                }
182                return EncodingEnum.XML;
183        }
184
185        public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq) {
186                ResponseEncoding retVal = determineRequestEncodingNoDefaultReturnRE(theReq);
187                if (retVal == null) {
188                        return null;
189                }
190                return retVal.getEncoding();
191        }
192
193        private static ResponseEncoding determineRequestEncodingNoDefaultReturnRE(RequestDetails theReq) {
194                ResponseEncoding retVal = null;
195                List<String> headers = theReq.getHeaders(Constants.HEADER_CONTENT_TYPE);
196                if (headers != null) {
197                        Iterator<String> acceptValues = headers.iterator();
198                        if (acceptValues != null) {
199                                while (acceptValues.hasNext() && retVal == null) {
200                                        String nextAcceptHeaderValue = acceptValues.next();
201                                        if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) {
202                                                for (String nextPart : nextAcceptHeaderValue.split(",")) {
203                                                        int scIdx = nextPart.indexOf(';');
204                                                        if (scIdx == 0) {
205                                                                continue;
206                                                        }
207                                                        if (scIdx != -1) {
208                                                                nextPart = nextPart.substring(0, scIdx);
209                                                        }
210                                                        nextPart = nextPart.trim();
211                                                        EncodingEnum encoding = EncodingEnum.forContentType(nextPart);
212                                                        if (encoding != null) {
213                                                                retVal = new ResponseEncoding(theReq.getServer().getFhirContext(), encoding, nextPart);
214                                                                break;
215                                                        }
216                                                }
217                                        }
218                                }
219                        }
220                }
221                return retVal;
222        }
223
224        /**
225         * Returns null if the request doesn't express that it wants FHIR. If it expresses that it wants XML and JSON
226         * equally, returns thePrefer.
227         */
228        public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) {
229                String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT);
230                if (format != null) {
231                        for (String nextFormat : format) {
232                                EncodingEnum retVal = EncodingEnum.forContentType(nextFormat);
233                                if (retVal != null) {
234                                        return new ResponseEncoding(theReq.getServer().getFhirContext(), retVal, nextFormat);
235                                }
236                        }
237                }
238
239                /*
240                 * Some browsers (e.g. FF) request "application/xml" in their Accept header,
241                 * and we generally want to treat this as a preference for FHIR XML even if
242                 * it's not the FHIR version of the CT, which should be "application/xml+fhir".
243                 * 
244                 * When we're serving up Binary resources though, we are a bit more strict,
245                 * since Binary is supposed to use native content types unless the client has
246                 * explicitly requested FHIR.
247                 */
248                boolean strict = false;
249                if ("Binary".equals(theReq.getResourceName())) {
250                        strict = true;
251                }
252
253                /*
254                 * The Accept header is kind of ridiculous, e.g.
255                 */
256                // text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8, image/png, */*;q=0.5
257
258                List<String> acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT);
259                float bestQ = -1f;
260                ResponseEncoding retVal = null;
261                if (acceptValues != null) {
262                        for (String nextAcceptHeaderValue : acceptValues) {
263                                StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, ",");
264                                while (tok.hasMoreTokens()) {
265                                        String nextToken = tok.nextToken();
266                                        int startSpaceIndex = -1;
267                                        for (int i = 0; i < nextToken.length(); i++) {
268                                                if (nextToken.charAt(i) != ' ') {
269                                                        startSpaceIndex = i;
270                                                        break;
271                                                }
272                                        }
273
274                                        if (startSpaceIndex == -1) {
275                                                continue;
276                                        }
277
278                                        int endSpaceIndex = -1;
279                                        for (int i = startSpaceIndex; i < nextToken.length(); i++) {
280                                                if (nextToken.charAt(i) == ' ' || nextToken.charAt(i) == ';') {
281                                                        endSpaceIndex = i;
282                                                        break;
283                                                }
284                                        }
285
286                                        float q = 1.0f;
287                                        ResponseEncoding encoding;
288                                        if (endSpaceIndex == -1) {
289                                                if (startSpaceIndex == 0) {
290                                                        encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken);
291                                                } else {
292                                                        encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex));
293                                                }
294                                        } else {
295                                                encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex));
296                                                String remaining = nextToken.substring(endSpaceIndex + 1);
297                                                StringTokenizer qualifierTok = new StringTokenizer(remaining, ";");
298                                                while (qualifierTok.hasMoreTokens()) {
299                                                        String nextQualifier = qualifierTok.nextToken();
300                                                        int equalsIndex = nextQualifier.indexOf('=');
301                                                        if (equalsIndex != -1) {
302                                                                String nextQualifierKey = nextQualifier.substring(0, equalsIndex).trim();
303                                                                String nextQualifierValue = nextQualifier.substring(equalsIndex + 1, nextQualifier.length()).trim();
304                                                                if (nextQualifierKey.equals("q")) {
305                                                                        try {
306                                                                                q = Float.parseFloat(nextQualifierValue);
307                                                                                q = Math.max(q, 0.0f);
308                                                                        } catch (NumberFormatException e) {
309                                                                                ourLog.debug("Invalid Accept header q value: {}", nextQualifierValue);
310                                                                        }
311                                                                }
312                                                        }
313                                                }
314                                        }
315
316                                        if (encoding != null) {
317                                                if (q > bestQ || (q == bestQ && encoding.getEncoding() == thePrefer)) {
318                                                        retVal = encoding;
319                                                        bestQ = q;
320                                                }
321                                        }
322
323                                }
324
325                        }
326
327                }
328
329                /*
330                 * If the client hasn't given any indication about which response
331                 * encoding they want, let's try the request encoding in case that
332                 * is useful (basically this catches the case where the request
333                 * has a Content-Type header but not an Accept header)
334                 */
335                if (retVal == null) {
336                        retVal = determineRequestEncodingNoDefaultReturnRE(theReq);
337                }
338
339                return retVal;
340        }
341
342        /**
343         * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's
344         * <code>"_format"</code> parameter and <code>"Accept:"</code> HTTP header.
345         */
346        public static ResponseEncoding determineResponseEncodingWithDefault(RequestDetails theReq) {
347                ResponseEncoding retVal = determineResponseEncodingNoDefault(theReq, theReq.getServer().getDefaultResponseEncoding());
348                if (retVal == null) {
349                        retVal = new ResponseEncoding(theReq.getServer().getFhirContext(), theReq.getServer().getDefaultResponseEncoding(), null);
350                }
351                return retVal;
352        }
353
354        public static Set<SummaryEnum> determineSummaryMode(RequestDetails theRequest) {
355                Map<String, String[]> requestParams = theRequest.getParameters();
356
357                Set<SummaryEnum> retVal = SummaryEnumParameter.getSummaryValueOrNull(theRequest);
358
359                if (retVal == null) {
360                        /*
361                         * HAPI originally supported a custom parameter called _narrative, but this has been superceded by an official
362                         * parameter called _summary
363                         */
364                        String[] narrative = requestParams.get(Constants.PARAM_NARRATIVE);
365                        if (narrative != null && narrative.length > 0) {
366                                try {
367                                        NarrativeModeEnum narrativeMode = NarrativeModeEnum.valueOfCaseInsensitive(narrative[0]);
368                                        switch (narrativeMode) {
369                                        case NORMAL:
370                                                retVal = Collections.singleton(SummaryEnum.FALSE);
371                                                break;
372                                        case ONLY:
373                                                retVal = Collections.singleton(SummaryEnum.TEXT);
374                                                break;
375                                        case SUPPRESS:
376                                                retVal = Collections.singleton(SummaryEnum.DATA);
377                                                break;
378                                        }
379                                } catch (IllegalArgumentException e) {
380                                        ourLog.debug("Invalid {} parameter: {}", Constants.PARAM_NARRATIVE, narrative[0]);
381                                }
382                        }
383                }
384                if (retVal == null) {
385                        retVal = Collections.singleton(SummaryEnum.FALSE);
386                }
387
388                return retVal;
389        }
390
391        public static Integer extractCountParameter(RequestDetails theRequest) {
392                return RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_COUNT);
393        }
394
395        public static IPrimitiveType<Date> extractLastUpdatedFromResource(IBaseResource theResource) {
396                IPrimitiveType<Date> lastUpdated = null;
397                if (theResource instanceof IResource) {
398                        lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) theResource);
399                } else if (theResource instanceof IAnyResource) {
400                        lastUpdated = new InstantDt(((IAnyResource) theResource).getMeta().getLastUpdated());
401                }
402                return lastUpdated;
403        }
404
405        public static IIdType fullyQualifyResourceIdOrReturnNull(IRestfulServerDefaults theServer, IBaseResource theResource, String theServerBase, IIdType theResourceId) {
406                IIdType retVal = null;
407                if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) {
408                        String resName = theResourceId.getResourceType();
409                        if (theResource != null && isBlank(resName)) {
410                                resName = theServer.getFhirContext().getResourceDefinition(theResource).getName();
411                        }
412                        if (isNotBlank(resName)) {
413                                retVal = theResourceId.withServerBase(theServerBase, resName);
414                        }
415                }
416                return retVal;
417        }
418
419        private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) {
420                EncodingEnum encoding;
421                if (theStrict) {
422                        encoding = EncodingEnum.forContentTypeStrict(theContentType);
423                } else {
424                        encoding = EncodingEnum.forContentType(theContentType);
425                }
426                if (encoding == null) {
427                        return null;
428                }
429                return new ResponseEncoding(theFhirContext, encoding, theContentType);
430        }
431
432        public static IParser getNewParser(FhirContext theContext, RequestDetails theRequestDetails) {
433
434                // Determine response encoding
435                EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails).getEncoding();
436                IParser parser;
437                switch (responseEncoding) {
438                case JSON:
439                        parser = theContext.newJsonParser();
440                        break;
441                case XML:
442                default:
443                        parser = theContext.newXmlParser();
444                        break;
445                }
446
447                configureResponseParser(theRequestDetails, parser);
448
449                return parser;
450        }
451
452        public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) {
453                Set<String> retVal = new HashSet<String>();
454
455                Enumeration<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT);
456                if (acceptValues != null) {
457                        float bestQ = -1f;
458                        while (acceptValues.hasMoreElements()) {
459                                String nextAcceptHeaderValue = acceptValues.nextElement();
460                                Matcher m = ACCEPT_HEADER_PATTERN.matcher(nextAcceptHeaderValue);
461                                float q = 1.0f;
462                                while (m.find()) {
463                                        String contentTypeGroup = m.group(1);
464                                        if (isNotBlank(contentTypeGroup)) {
465
466                                                String name = m.group(3);
467                                                String value = m.group(4);
468                                                if (name != null && value != null) {
469                                                        if ("q".equals(name)) {
470                                                                try {
471                                                                        q = Float.parseFloat(value);
472                                                                        q = Math.max(q, 0.0f);
473                                                                } catch (NumberFormatException e) {
474                                                                        ourLog.debug("Invalid Accept header q value: {}", value);
475                                                                }
476                                                        }
477                                                }
478
479                                                if (q > bestQ) {
480                                                        retVal.clear();
481                                                        bestQ = q;
482                                                }
483
484                                                if (q == bestQ) {
485                                                        retVal.add(contentTypeGroup.trim());
486                                                }
487
488                                        }
489
490                                        if (!",".equals(m.group(5))) {
491                                                break;
492                                        }
493                                }
494
495                        }
496                }
497
498                return retVal;
499        }
500
501        public static PreferReturnEnum parsePreferHeader(String theValue) {
502                if (isBlank(theValue)) {
503                        return null;
504                }
505
506                StringTokenizer tok = new StringTokenizer(theValue, ",");
507                while (tok.hasMoreTokens()) {
508                        String next = tok.nextToken();
509                        int eqIndex = next.indexOf('=');
510                        if (eqIndex == -1 || eqIndex >= next.length() - 2) {
511                                continue;
512                        }
513
514                        String key = next.substring(0, eqIndex).trim();
515                        if (key.equals(Constants.HEADER_PREFER_RETURN) == false) {
516                                continue;
517                        }
518
519                        String value = next.substring(eqIndex + 1).trim();
520                        if (value.length() < 2) {
521                                continue;
522                        }
523                        if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
524                                value = value.substring(1, value.length() - 1);
525                        }
526
527                        return PreferReturnEnum.fromHeaderValue(value);
528                }
529
530                return null;
531        }
532
533        public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) {
534                Map<String, String[]> requestParams = theRequest.getParameters();
535                String[] pretty = requestParams.get(Constants.PARAM_PRETTY);
536                boolean prettyPrint;
537                if (pretty != null && pretty.length > 0) {
538                        if (Constants.PARAM_PRETTY_VALUE_TRUE.equals(pretty[0])) {
539                                prettyPrint = true;
540                        } else {
541                                prettyPrint = false;
542                        }
543                } else {
544                        prettyPrint = theServer.isDefaultPrettyPrint();
545                        List<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT);
546                        if (acceptValues != null) {
547                                for (String nextAcceptHeaderValue : acceptValues) {
548                                        if (nextAcceptHeaderValue.contains("pretty=true")) {
549                                                prettyPrint = true;
550                                        }
551                                }
552                        }
553                }
554                return prettyPrint;
555        }
556
557        public static Object streamResponseAsBundle(IRestfulServerDefaults theServer, Bundle bundle, Set<SummaryEnum> theSummaryMode, boolean respondGzip, RequestDetails theRequestDetails)
558                        throws IOException {
559
560                int status = 200;
561
562                // Determine response encoding
563                EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails).getEncoding();
564
565                String contentType = responseEncoding.getBundleContentType();
566
567                String charset = Constants.CHARSET_NAME_UTF8;
568                Writer writer = theRequestDetails.getResponse().getResponseWriter(status, null, contentType, charset, respondGzip);
569
570                try {
571                        IParser parser = RestfulServerUtils.getNewParser(theServer.getFhirContext(), theRequestDetails);
572                        if (theSummaryMode.contains(SummaryEnum.TEXT)) {
573                                parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
574                        }
575                        parser.encodeBundleToWriter(bundle, writer);
576                } catch (Exception e) {
577                        // always send a response, even if the parsing went wrong
578                }
579                //FIXME resource leak
580                return theRequestDetails.getResponse().sendWriterResponse(status, contentType, charset, writer);
581        }
582
583        public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader,
584                        boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
585                return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
586        }
587
588        public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStausCode, String theStatusMessage,
589                        boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
590                        throws IOException {
591                IRestfulResponse restUtil = theRequestDetails.getResponse();
592
593                // Determine response encoding
594                ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theServer.getDefaultResponseEncoding());
595
596                String serverBase = theRequestDetails.getFhirServerBase();
597                IIdType fullId = null;
598                if (theOperationResourceId != null) {
599                        fullId = theOperationResourceId;
600                } else if (theResource != null) {
601                        if (theResource.getIdElement() != null) {
602                                IIdType resourceId = theResource.getIdElement();
603                                fullId = fullyQualifyResourceIdOrReturnNull(theServer, theResource, serverBase, resourceId);
604                        }
605                }
606
607                if (theAddContentLocationHeader && fullId != null) {
608                        if (theServer.getFhirContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
609                                restUtil.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue());
610                        }
611                        restUtil.addHeader(Constants.HEADER_LOCATION, fullId.getValue());
612                }
613
614                if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) {
615                        if (fullId != null && fullId.hasVersionIdPart()) {
616                                restUtil.addHeader(Constants.HEADER_ETAG, "W/\"" + fullId.getVersionIdPart() + '"');
617                        }
618                }
619
620                String contentType;
621                if (theResource instanceof IBaseBinary && responseEncoding == null) {
622                        IBaseBinary bin = (IBaseBinary) theResource;
623                        if (isNotBlank(bin.getContentType())) {
624                                contentType = bin.getContentType();
625                        } else {
626                                contentType = Constants.CT_OCTET_STREAM;
627                        }
628                        // Force binary resources to download - This is a security measure to prevent
629                        // malicious images or HTML blocks being served up as content.
630                        restUtil.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
631
632                        return restUtil.sendAttachmentResponse(bin, theStausCode, contentType);
633                }
634
635                // Ok, we're not serving a binary resource, so apply default encoding
636                if (responseEncoding == null) {
637                        responseEncoding = new ResponseEncoding(theServer.getFhirContext(), theServer.getDefaultResponseEncoding(), null);
638                }
639
640                boolean encodingDomainResourceAsText = theSummaryMode.contains(SummaryEnum.TEXT);
641                if (encodingDomainResourceAsText) {
642                        /*
643                         * If the user requests "text" for a bundle, only suppress the non text elements in the Element.entry.resource
644                         * parts, we're not streaming just the narrative as HTML (since bundles don't even
645                         * have one)
646                         */
647                        if ("Bundle".equals(theServer.getFhirContext().getResourceDefinition(theResource).getName())) {
648                                encodingDomainResourceAsText = false;
649                        }
650                }
651
652                /*
653                 * Last-Modified header
654                 */
655
656                IPrimitiveType<Date> lastUpdated;
657                if (theOperationResourceLastUpdated != null) {
658                        lastUpdated = theOperationResourceLastUpdated;
659                } else {
660                        lastUpdated = extractLastUpdatedFromResource(theResource);
661                }
662                if (lastUpdated != null && lastUpdated.isEmpty() == false) {
663                        restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue()));
664                }
665
666                /*
667                 * Category header (DSTU1 only)
668                 */
669
670                if (theResource instanceof IResource && theServer.getFhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) {
671                        TagList list = (TagList) ((IResource) theResource).getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST);
672                        if (list != null) {
673                                for (Tag tag : list) {
674                                        if (StringUtils.isNotBlank(tag.getTerm())) {
675                                                restUtil.addHeader(Constants.HEADER_CATEGORY, tag.toHeaderValue());
676                                        }
677                                }
678                        }
679                }
680
681                /*
682                 * Stream the response body
683                 */
684
685                if (theResource == null) {
686                        contentType = null;
687                } else if (encodingDomainResourceAsText) {
688                        contentType = Constants.CT_HTML;
689                } else {
690                        contentType = responseEncoding.getResourceContentType();
691                }
692                String charset = Constants.CHARSET_NAME_UTF8;
693
694                Writer writer = restUtil.getResponseWriter(theStausCode, theStatusMessage, contentType, charset, respondGzip);
695                if (theResource == null) {
696                        // No response is being returned
697                } else if (encodingDomainResourceAsText && theResource instanceof IResource) {
698                        writer.append(((IResource) theResource).getText().getDiv().getValueAsString());
699                } else {
700                        IParser parser = getNewParser(theServer.getFhirContext(), theRequestDetails);
701                        parser.encodeResourceToWriter(theResource, writer);
702                }
703                //FIXME resource leak
704                return restUtil.sendWriterResponse(theStausCode, contentType, charset, writer);
705        }
706
707        public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) {
708                String[] retVal = theRequest.getParameters().get(theParamName);
709                if (retVal == null) {
710                        return null;
711                }
712                try {
713                        return Integer.parseInt(retVal[0]);
714                } catch (NumberFormatException e) {
715                        ourLog.debug("Failed to parse {} value '{}': {}", new Object[] { theParamName, retVal[0], e });
716                        return null;
717                }
718        }
719
720        // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) {
721        // String countString = theRequest.getParameter(name);
722        // Integer count = null;
723        // if (isNotBlank(countString)) {
724        // try {
725        // count = Integer.parseInt(countString);
726        // } catch (NumberFormatException e) {
727        // ourLog.debug("Failed to parse _count value '{}': {}", countString, e);
728        // }
729        // }
730        // return count;
731        // }
732
733        public static void validateResourceListNotNull(List<? extends IBaseResource> theResourceList) {
734                if (theResourceList == null) {
735                        throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed");
736                }
737        }
738
739        private static enum NarrativeModeEnum {
740                NORMAL, ONLY, SUPPRESS;
741
742                public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) {
743                        return valueOf(NarrativeModeEnum.class, theCode.toUpperCase());
744                }
745        }
746
747        /**
748         * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)}
749         */
750        public static class ResponseEncoding {
751                private final EncodingEnum myEncoding;
752                private final Boolean myNonLegacy;
753
754                public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) {
755                        super();
756                        myEncoding = theEncoding;
757                        if (theContentType != null) {
758                                if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) {
759                                        FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
760                                        myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU3)
761                                                        || (ctxtEnum.isEquivalentTo(FhirVersionEnum.DSTU3) && !"1.4.0".equals(theCtx.getVersion().getVersion().getFhirVersionString()));
762                                } else {
763                                        myNonLegacy = EncodingEnum.isNonLegacy(theContentType);
764                                }
765                        } else {
766                                FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
767                                if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)
768                                        || (ctxtEnum.isEquivalentTo(FhirVersionEnum.DSTU3) && "1.4.0".equals(theCtx.getVersion().getVersion().getFhirVersionString()))) {
769                                        myNonLegacy = null;
770                                } else {
771                                        myNonLegacy = Boolean.TRUE;
772                                }
773                        }
774                }
775
776                public EncodingEnum getEncoding() {
777                        return myEncoding;
778                }
779
780                public String getResourceContentType() {
781                        if (Boolean.TRUE.equals(isNonLegacy())) {
782                                return getEncoding().getResourceContentTypeNonLegacy();
783                        }
784                        return getEncoding().getResourceContentType();
785                }
786
787                public Boolean isNonLegacy() {
788                        return myNonLegacy;
789                }
790        }
791
792        public static void addAcceptHeaderToRequest(EncodingEnum theEncoding, IHttpRequest theHttpRequest, FhirContext theContext) {
793                if (theEncoding == null) {
794                        if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1) == false) {
795                                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_LEGACY);
796                        } else {
797                                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY);
798                        }
799                } else if (theEncoding == EncodingEnum.JSON) {
800                        if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1) == false) {
801                                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON);
802                        } else {
803                                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY);
804                        }
805                } else if (theEncoding == EncodingEnum.XML) {
806                        if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1) == false) {
807                                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_XML);
808                        } else {
809                                theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY);
810                        }
811                }
812
813        }
814
815}