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