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