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