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