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