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.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.api.AddProfileTagEnum;
026import ca.uhn.fhir.context.api.BundleInclusionRule;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.interceptor.api.HookParams;
029import ca.uhn.fhir.interceptor.api.IInterceptorService;
030import ca.uhn.fhir.interceptor.api.Pointcut;
031import ca.uhn.fhir.interceptor.executor.InterceptorService;
032import ca.uhn.fhir.model.primitive.InstantDt;
033import ca.uhn.fhir.parser.IParser;
034import ca.uhn.fhir.rest.annotation.Destroy;
035import ca.uhn.fhir.rest.annotation.IdParam;
036import ca.uhn.fhir.rest.annotation.Initialize;
037import ca.uhn.fhir.rest.api.Constants;
038import ca.uhn.fhir.rest.api.EncodingEnum;
039import ca.uhn.fhir.rest.api.MethodOutcome;
040import ca.uhn.fhir.rest.api.PreferReturnEnum;
041import ca.uhn.fhir.rest.api.RequestTypeEnum;
042import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
043import ca.uhn.fhir.rest.api.server.BaseParseAction;
044import ca.uhn.fhir.rest.api.server.IFhirVersionServer;
045import ca.uhn.fhir.rest.api.server.IRestfulServer;
046import ca.uhn.fhir.rest.api.server.RequestDetails;
047import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
048import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
049import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
050import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
051import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
052import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
053import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
054import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
055import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
056import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
057import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding;
058import ca.uhn.fhir.rest.server.method.MethodMatchEnum;
059import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
060import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
061import ca.uhn.fhir.util.CoverageIgnore;
062import ca.uhn.fhir.util.OperationOutcomeUtil;
063import ca.uhn.fhir.util.ReflectionUtil;
064import ca.uhn.fhir.util.UrlPathTokenizer;
065import ca.uhn.fhir.util.UrlUtil;
066import ca.uhn.fhir.util.VersionUtil;
067import com.google.common.collect.Lists;
068import jakarta.annotation.Nonnull;
069import jakarta.annotation.Nullable;
070import jakarta.servlet.ServletException;
071import jakarta.servlet.UnavailableException;
072import jakarta.servlet.http.HttpServlet;
073import jakarta.servlet.http.HttpServletRequest;
074import jakarta.servlet.http.HttpServletResponse;
075import org.apache.commons.lang3.RandomStringUtils;
076import org.apache.commons.lang3.StringUtils;
077import org.apache.commons.lang3.Validate;
078import org.hl7.fhir.instance.model.api.IBaseConformance;
079import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
080import org.hl7.fhir.instance.model.api.IBaseResource;
081import org.hl7.fhir.instance.model.api.IIdType;
082import org.slf4j.Logger;
083import org.slf4j.LoggerFactory;
084
085import java.io.IOException;
086import java.io.InputStream;
087import java.io.Writer;
088import java.lang.annotation.Annotation;
089import java.lang.reflect.Method;
090import java.lang.reflect.Modifier;
091import java.util.ArrayList;
092import java.util.Arrays;
093import java.util.Collection;
094import java.util.Collections;
095import java.util.Date;
096import java.util.HashMap;
097import java.util.HashSet;
098import java.util.Iterator;
099import java.util.List;
100import java.util.ListIterator;
101import java.util.Map;
102import java.util.Map.Entry;
103import java.util.Set;
104import java.util.concurrent.locks.Lock;
105import java.util.concurrent.locks.ReentrantLock;
106import java.util.jar.Manifest;
107
108import static ca.uhn.fhir.util.StringUtil.toUtf8String;
109import static java.util.stream.Collectors.toList;
110import static org.apache.commons.lang3.StringUtils.isBlank;
111import static org.apache.commons.lang3.StringUtils.isNotBlank;
112
113/**
114 * This class is the central class for the HAPI FHIR Plain Server framework.
115 * <p>
116 * See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/">HAPI FHIR Plain Server</a>
117 * for information on how to use this framework.
118 */
119@SuppressWarnings("WeakerAccess")
120public class RestfulServer extends HttpServlet implements IRestfulServer<ServletRequestDetails> {
121
122        /**
123         * All incoming requests will have an attribute added to {@link HttpServletRequest#getAttribute(String)}
124         * with this key. The value will be a Java {@link Date} with the time that request processing began.
125         */
126        public static final String REQUEST_START_TIME = RestfulServer.class.getName() + "REQUEST_START_TIME";
127        /**
128         * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED}
129         */
130        public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED;
131        /**
132         * Requests will have an HttpServletRequest attribute set with this name, containing the servlet
133         * context, in order to avoid a dependency on Servlet-API 3.0+
134         */
135        public static final String SERVLET_CONTEXT_ATTRIBUTE = "ca.uhn.fhir.rest.server.RestfulServer.servlet_context";
136        /**
137         * Default value for {@link #setDefaultPreferReturn(PreferReturnEnum)}
138         */
139        public static final PreferReturnEnum DEFAULT_PREFER_RETURN = PreferReturnEnum.REPRESENTATION;
140
141        private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor();
142        private static final Logger ourLog = LoggerFactory.getLogger(RestfulServer.class);
143        private static final long serialVersionUID = 1L;
144        private final List<Object> myPlainProviders = new ArrayList<>();
145        private final List<IResourceProvider> myResourceProviders = new ArrayList<>();
146        private IInterceptorService myInterceptorService;
147        private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES;
148        private boolean myDefaultPrettyPrint = false;
149        private EncodingEnum myDefaultResponseEncoding = EncodingEnum.JSON;
150        private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT;
151        private FhirContext myFhirContext;
152        private boolean myIgnoreServerParsedRequestParameters = true;
153        private String myImplementationDescription;
154        private String myCopyright;
155        private IPagingProvider myPagingProvider;
156        private Integer myDefaultPageSize;
157        private Integer myMaximumPageSize;
158        private boolean myStatelessPagingDefault = false;
159        private Lock myProviderRegistrationMutex = new ReentrantLock();
160        private Map<String, ResourceBinding> myResourceNameToBinding = new HashMap<>();
161        private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
162        private ResourceBinding myServerBinding = new ResourceBinding();
163        private ResourceBinding myGlobalBinding = new ResourceBinding();
164        private ConformanceMethodBinding myServerConformanceMethod;
165        private Object myServerConformanceProvider;
166        private String myServerName = "HAPI FHIR Server";
167        /**
168         * This is configurable but by default we just use HAPI version
169         */
170        private String myServerVersion = createPoweredByHeaderProductVersion();
171
172        private boolean myStarted;
173        private boolean myUncompressIncomingContents = true;
174        private ITenantIdentificationStrategy myTenantIdentificationStrategy;
175        private PreferReturnEnum myDefaultPreferReturn = DEFAULT_PREFER_RETURN;
176        private ElementsSupportEnum myElementsSupport = ElementsSupportEnum.EXTENDED;
177
178        /**
179         * Constructor. Note that if no {@link FhirContext} is passed in to the server (either through the constructor, or
180         * through {@link #setFhirContext(FhirContext)}) the server will determine which
181         * version of FHIR to support through classpath scanning. This is brittle, and it is highly recommended to explicitly
182         * specify a FHIR version.
183         */
184        public RestfulServer() {
185                this(null);
186        }
187
188        /**
189         * Constructor
190         */
191        public RestfulServer(FhirContext theCtx) {
192                this(theCtx, new InterceptorService("RestfulServer"));
193        }
194
195        public RestfulServer(FhirContext theCtx, IInterceptorService theInterceptorService) {
196                myFhirContext = theCtx;
197                setInterceptorService(theInterceptorService);
198        }
199
200        /**
201         * @since 5.5.0
202         */
203        public ConformanceMethodBinding getServerConformanceMethod() {
204                return myServerConformanceMethod;
205        }
206
207        private void addContentLocationHeaders(
208                        RequestDetails theRequest,
209                        HttpServletResponse servletResponse,
210                        MethodOutcome response,
211                        String resourceName) {
212                if (response != null && response.getId() != null) {
213                        addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_LOCATION, resourceName);
214                        addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_CONTENT_LOCATION, resourceName);
215                }
216        }
217
218        /**
219         * This method is called prior to sending a response to incoming requests. It is used to add custom headers.
220         * <p>
221         * Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid
222         * inadvertently disabling functionality.
223         * </p>
224         */
225        public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
226                String poweredByHeader = createPoweredByHeader();
227                if (isNotBlank(poweredByHeader)) {
228                        theHttpResponse.addHeader(Constants.POWERED_BY_HEADER, poweredByHeader);
229                }
230        }
231
232        private void addLocationHeader(
233                        RequestDetails theRequest,
234                        HttpServletResponse theResponse,
235                        MethodOutcome response,
236                        String headerLocation,
237                        String resourceName) {
238                StringBuilder b = new StringBuilder();
239                b.append(theRequest.getFhirServerBase());
240                b.append('/');
241                b.append(resourceName);
242                b.append('/');
243                b.append(response.getId().getIdPart());
244                if (response.getId().hasVersionIdPart()) {
245                        b.append("/" + Constants.PARAM_HISTORY + "/");
246                        b.append(response.getId().getVersionIdPart());
247                }
248                theResponse.addHeader(headerLocation, b.toString());
249        }
250
251        public RestfulServerConfiguration createConfiguration() {
252                RestfulServerConfiguration result = new RestfulServerConfiguration();
253                result.setResourceBindings(getResourceBindings());
254                result.setServerBindings(getServerBindings());
255                result.setGlobalBindings(getGlobalBindings());
256                result.setImplementationDescription(getImplementationDescription());
257                result.setServerVersion(getServerVersion());
258                result.setServerName(getServerName());
259                result.setFhirContext(getFhirContext());
260                result.setServerAddressStrategy(myServerAddressStrategy);
261                try (InputStream inputStream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF")) {
262                        if (inputStream != null) {
263                                Manifest manifest = new Manifest(inputStream);
264                                String value = manifest.getMainAttributes().getValue("Build-Time");
265                                result.setConformanceDate(new InstantDt(value));
266                        }
267                } catch (Exception e) {
268                        // fall through
269                }
270                result.computeSharedSupertypeForResourcePerName(getResourceProviders());
271                return result;
272        }
273
274        private List<BaseMethodBinding> getGlobalBindings() {
275                return myGlobalBinding.getMethodBindings();
276        }
277
278        protected List<String> createPoweredByAttributes() {
279                return Lists.newArrayList(
280                                "FHIR Server",
281                                "FHIR " + myFhirContext.getVersion().getVersion().getFhirVersionString() + "/"
282                                                + myFhirContext.getVersion().getVersion().name());
283        }
284
285        /**
286         * Subclasses may override to provide their own powered by
287         * header. Note that if you want to be nice and still credit HAPI
288         * FHIR you could consider overriding
289         * {@link #createPoweredByAttributes()} instead and adding your own
290         * fragments to the list.
291         */
292        protected String createPoweredByHeader() {
293                StringBuilder b = new StringBuilder();
294                b.append(createPoweredByHeaderProductName());
295                b.append(" ");
296                b.append(createPoweredByHeaderProductVersion());
297                b.append(" ");
298                b.append(createPoweredByHeaderComponentName());
299                b.append(" (");
300
301                List<String> poweredByAttributes = createPoweredByAttributes();
302                for (ListIterator<String> iter = poweredByAttributes.listIterator(); iter.hasNext(); ) {
303                        if (iter.nextIndex() > 0) {
304                                b.append("; ");
305                        }
306                        b.append(iter.next());
307                }
308
309                b.append(")");
310                return b.toString();
311        }
312
313        /**
314         * Subclasses my override
315         *
316         * @see #createPoweredByHeader()
317         */
318        protected String createPoweredByHeaderComponentName() {
319                return "REST Server";
320        }
321
322        /**
323         * Subclasses my override
324         *
325         * @see #createPoweredByHeader()
326         */
327        protected String createPoweredByHeaderProductName() {
328                return "HAPI FHIR";
329        }
330
331        /**
332         * Subclasses my override
333         *
334         * @see #createPoweredByHeader()
335         */
336        protected String createPoweredByHeaderProductVersion() {
337                String version = VersionUtil.getVersion();
338                if (VersionUtil.isSnapshot()) {
339                        version = version + "/" + VersionUtil.getBuildNumber() + "/" + VersionUtil.getBuildDate();
340                }
341                return version;
342        }
343
344        @Override
345        public void destroy() {
346                if (getResourceProviders() != null) {
347                        for (IResourceProvider iResourceProvider : getResourceProviders()) {
348                                invokeDestroy(iResourceProvider);
349                        }
350                }
351
352                if (myServerConformanceProvider != null) {
353                        invokeDestroy(myServerConformanceProvider);
354                }
355
356                if (getPlainProviders() != null) {
357                        for (Object next : getPlainProviders()) {
358                                invokeDestroy(next);
359                        }
360                }
361
362                if (myServerConformanceMethod != null) {
363                        myServerConformanceMethod.close();
364                }
365                myResourceNameToBinding.values().stream()
366                                .flatMap(t -> t.getMethodBindings().stream())
367                                .forEach(t -> t.close());
368                myGlobalBinding.getMethodBindings().forEach(t -> t.close());
369                myServerBinding.getMethodBindings().forEach(t -> t.close());
370
371                myResourceNameToBinding.clear();
372                myGlobalBinding.getMethodBindings().clear();
373                myServerBinding.getMethodBindings().clear();
374        }
375
376        /**
377         * Figure out and return whichever method binding is appropriate for
378         * the given request
379         */
380        public BaseMethodBinding determineResourceMethod(RequestDetails requestDetails, String requestPath) {
381                RequestTypeEnum requestType = requestDetails.getRequestType();
382
383                ResourceBinding resourceBinding = null;
384                BaseMethodBinding resourceMethod = null;
385                String resourceName = requestDetails.getResourceName();
386                if (myServerConformanceMethod.incomingServerRequestMatchesMethod(requestDetails) != MethodMatchEnum.NONE) {
387                        resourceMethod = myServerConformanceMethod;
388                } else if (resourceName == null) {
389                        resourceBinding = myServerBinding;
390                } else {
391                        resourceBinding = myResourceNameToBinding.get(resourceName);
392                        if (resourceBinding == null) {
393                                throwUnknownResourceTypeException(resourceName);
394                        }
395                }
396
397                if (resourceMethod == null) {
398                        if (resourceBinding != null) {
399                                resourceMethod = resourceBinding.getMethod(requestDetails);
400                        }
401                        if (resourceMethod == null) {
402                                resourceMethod = myGlobalBinding.getMethod(requestDetails);
403                        }
404                }
405                if (resourceMethod == null) {
406                        if (isBlank(requestPath)) {
407                                throw new InvalidRequestException(
408                                                Msg.code(287) + myFhirContext.getLocalizer().getMessage(RestfulServer.class, "rootRequest"));
409                        }
410                        throwUnknownFhirOperationException(requestDetails, requestPath, requestType);
411                }
412                return resourceMethod;
413        }
414
415        @Override
416        protected void doDelete(HttpServletRequest request, HttpServletResponse response)
417                        throws ServletException, IOException {
418                handleRequest(RequestTypeEnum.DELETE, request, response);
419        }
420
421        @Override
422        protected void doGet(HttpServletRequest request, HttpServletResponse response)
423                        throws ServletException, IOException {
424                handleRequest(RequestTypeEnum.GET, request, response);
425        }
426
427        @Override
428        protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp)
429                        throws ServletException, IOException {
430                handleRequest(RequestTypeEnum.OPTIONS, theReq, theResp);
431        }
432
433        @Override
434        protected void doPost(HttpServletRequest request, HttpServletResponse response)
435                        throws ServletException, IOException {
436                handleRequest(RequestTypeEnum.POST, request, response);
437        }
438
439        @Override
440        protected void doPut(HttpServletRequest request, HttpServletResponse response)
441                        throws ServletException, IOException {
442                handleRequest(RequestTypeEnum.PUT, request, response);
443        }
444
445        private void findResourceMethods(Object theProvider) {
446
447                ourLog.debug("Scanning type for RESTful methods: {}", theProvider.getClass());
448                int count = 0;
449
450                Class<?> clazz = theProvider.getClass();
451                Class<?> supertype = clazz.getSuperclass();
452                while (!Object.class.equals(supertype)) {
453                        count += findResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces());
454                        count += findResourceMethods(theProvider, supertype);
455                        supertype = supertype.getSuperclass();
456                }
457
458                try {
459                        count += findResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces());
460                        count += findResourceMethods(theProvider, clazz);
461                } catch (ConfigurationException e) {
462                        throw new ConfigurationException(
463                                        Msg.code(288) + "Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e);
464                }
465                if (count == 0) {
466                        throw new ConfigurationException(
467                                        Msg.code(289) + "Did not find any annotated RESTful methods on provider class "
468                                                        + theProvider.getClass().getName());
469                }
470        }
471
472        private int findResourceMethodsOnInterfaces(Object theProvider, Class<?>[] interfaces) {
473                int count = 0;
474                for (Class<?> anInterface : interfaces) {
475                        count += findResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces());
476                        count += findResourceMethods(theProvider, anInterface);
477                }
478                return count;
479        }
480
481        private int findResourceMethods(Object theProvider, Class<?> clazz) throws ConfigurationException {
482                int count = 0;
483
484                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
485                        BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
486                        if (foundMethodBinding == null) {
487                                continue;
488                        }
489
490                        count++;
491
492                        if (foundMethodBinding instanceof ConformanceMethodBinding) {
493                                myServerConformanceMethod = (ConformanceMethodBinding) foundMethodBinding;
494                                if (myServerConformanceProvider == null) {
495                                        myServerConformanceProvider = theProvider;
496                                }
497                                continue;
498                        }
499
500                        if (!Modifier.isPublic(m.getModifiers())) {
501                                throw new ConfigurationException(Msg.code(290) + "Method '" + m.getName()
502                                                + "' is not public, FHIR RESTful methods must be public");
503                        }
504                        if (Modifier.isStatic(m.getModifiers())) {
505                                throw new ConfigurationException(Msg.code(291) + "Method '" + m.getName()
506                                                + "' is static, FHIR RESTful methods must not be static");
507                        }
508                        ourLog.trace("Scanning public method: {}#{}", theProvider.getClass(), m.getName());
509
510                        // Interceptor call: SERVER_PROVIDER_METHOD_BOUND
511                        if (myInterceptorService.hasHooks(Pointcut.SERVER_PROVIDER_METHOD_BOUND)) {
512                                HookParams params = new HookParams().add(BaseMethodBinding.class, foundMethodBinding);
513                                BaseMethodBinding newMethodBinding = (BaseMethodBinding)
514                                                myInterceptorService.callHooksAndReturnObject(Pointcut.SERVER_PROVIDER_METHOD_BOUND, params);
515                                if (newMethodBinding == null) {
516                                        ourLog.info(
517                                                        "Method binding {} was discarded by interceptor and will not be registered",
518                                                        foundMethodBinding);
519                                        continue;
520                                }
521                                foundMethodBinding = newMethodBinding;
522                        }
523
524                        String resourceName = foundMethodBinding.getResourceName();
525                        ResourceBinding resourceBinding;
526                        if (resourceName == null) {
527                                if (foundMethodBinding.isGlobalMethod()) {
528                                        resourceBinding = myGlobalBinding;
529                                } else {
530                                        resourceBinding = myServerBinding;
531                                }
532                        } else {
533                                RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName);
534                                if (myResourceNameToBinding.containsKey(definition.getName())) {
535                                        resourceBinding = myResourceNameToBinding.get(definition.getName());
536                                } else {
537                                        resourceBinding = new ResourceBinding();
538                                        resourceBinding.setResourceName(resourceName);
539                                        myResourceNameToBinding.put(resourceName, resourceBinding);
540                                }
541                        }
542
543                        List<Class<?>> allowableParams = foundMethodBinding.getAllowableParamAnnotations();
544                        if (allowableParams != null) {
545                                for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) {
546                                        for (Annotation annotation : nextParamAnnotations) {
547                                                Package pack = annotation.annotationType().getPackage();
548                                                if (pack.equals(IdParam.class.getPackage())) {
549                                                        if (!allowableParams.contains(annotation.annotationType())) {
550                                                                throw new ConfigurationException(Msg.code(292) + "Method[" + m
551                                                                                + "] is not allowed to have a parameter annotated with " + annotation);
552                                                        }
553                                                }
554                                        }
555                                }
556                        }
557
558                        resourceBinding.addMethod(foundMethodBinding);
559                        ourLog.trace(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName());
560                }
561
562                return count;
563        }
564
565        /**
566         * @deprecated As of HAPI FHIR 1.5, this property has been moved to
567         * {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)}
568         */
569        @Override
570        @Deprecated
571        public AddProfileTagEnum getAddProfileTag() {
572                return myFhirContext.getAddProfileTagWhenEncoding();
573        }
574
575        /**
576         * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER}
577         * (which is the default), the server will automatically add a profile tag based on
578         * the class of the resource(s) being returned.
579         *
580         * @param theAddProfileTag The behaviour enum (must not be null)
581         * @deprecated As of HAPI FHIR 1.5, this property has been moved to
582         * {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)}
583         */
584        @Deprecated
585        @CoverageIgnore
586        public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) {
587                Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null");
588                myFhirContext.setAddProfileTagWhenEncoding(theAddProfileTag);
589        }
590
591        @Override
592        public BundleInclusionRule getBundleInclusionRule() {
593                return myBundleInclusionRule;
594        }
595
596        /**
597         * Set how bundle factory should decide whether referenced resources should be included in bundles
598         *
599         * @param theBundleInclusionRule - inclusion rule (@see BundleInclusionRule for behaviors)
600         */
601        public void setBundleInclusionRule(BundleInclusionRule theBundleInclusionRule) {
602                myBundleInclusionRule = theBundleInclusionRule;
603        }
604
605        /**
606         * Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either
607         * with the <code>_format</code> URL parameter, or with an <code>Accept</code> header
608         * in the request. The default is {@link EncodingEnum#XML}. Will not return null.
609         */
610        @Override
611        public EncodingEnum getDefaultResponseEncoding() {
612                return myDefaultResponseEncoding;
613        }
614
615        /**
616         * Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with
617         * the <code>_format</code> URL parameter, or with an <code>Accept</code> header in
618         * the request. The default is {@link EncodingEnum#XML}.
619         * <p>
620         * Note when testing this feature: Some browsers will include "application/xml" in their Accept header, which means
621         * that the
622         * </p>
623         */
624        public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) {
625                Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null");
626                myDefaultResponseEncoding = theDefaultResponseEncoding;
627        }
628
629        @Override
630        public ETagSupportEnum getETagSupport() {
631                return myETagSupport;
632        }
633
634        /**
635         * Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is
636         * {@link #DEFAULT_ETAG_SUPPORT}
637         *
638         * @param theETagSupport The ETag support mode
639         */
640        public void setETagSupport(ETagSupportEnum theETagSupport) {
641                if (theETagSupport == null) {
642                        throw new NullPointerException(Msg.code(293) + "theETagSupport can not be null");
643                }
644                myETagSupport = theETagSupport;
645        }
646
647        @Override
648        public ElementsSupportEnum getElementsSupport() {
649                return myElementsSupport;
650        }
651
652        /**
653         * Sets the elements support mode.
654         *
655         * @see <a href="http://hapifhir.io/doc_rest_server.html#extended_elements_support">Extended Elements Support</a>
656         */
657        public void setElementsSupport(ElementsSupportEnum theElementsSupport) {
658                Validate.notNull(theElementsSupport, "theElementsSupport must not be null");
659                myElementsSupport = theElementsSupport;
660        }
661
662        /**
663         * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain
664         * providers should generally use this context if one is needed, as opposed to
665         * creating their own.
666         */
667        @Override
668        public FhirContext getFhirContext() {
669                if (myFhirContext == null) {
670                        // TODO: Use of a deprecated method should be resolved.
671                        myFhirContext = new FhirContext();
672                }
673                return myFhirContext;
674        }
675
676        public void setFhirContext(FhirContext theFhirContext) {
677                Validate.notNull(theFhirContext, "FhirContext must not be null");
678                myFhirContext = theFhirContext;
679        }
680
681        public String getImplementationDescription() {
682                return myImplementationDescription;
683        }
684
685        public void setImplementationDescription(String theImplementationDescription) {
686                myImplementationDescription = theImplementationDescription;
687        }
688
689        /**
690         * Returns the server copyright (will be added to the CapabilityStatement). Note that FHIR allows Markdown in this string.
691         */
692        public String getCopyright() {
693                return myCopyright;
694        }
695
696        /**
697         * Sets the server copyright (will be added to the CapabilityStatement). Note that FHIR allows Markdown in this string.
698         */
699        public void setCopyright(String theCopyright) {
700                myCopyright = theCopyright;
701        }
702
703        /**
704         * Returns a list of all registered server interceptors
705         *
706         * @deprecated As of HAPI FHIR 3.8.0, use {@link #getInterceptorService()} to access the interceptor service. You can register and unregister interceptors using this service.
707         */
708        @Deprecated
709        @Override
710        public List<IServerInterceptor> getInterceptors_() {
711                List<IServerInterceptor> retVal = getInterceptorService().getAllRegisteredInterceptors().stream()
712                                .filter(t -> t instanceof IServerInterceptor)
713                                .map(t -> (IServerInterceptor) t)
714                                .collect(toList());
715                return Collections.unmodifiableList(retVal);
716        }
717
718        /**
719         * Returns the interceptor registry for this service. Use this registry to register and unregister
720         *
721         * @since 3.8.0
722         */
723        @Override
724        public IInterceptorService getInterceptorService() {
725                return myInterceptorService;
726        }
727
728        /**
729         * Sets the interceptor registry for this service. Use this registry to register and unregister
730         *
731         * @since 3.8.0
732         */
733        public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) {
734                Validate.notNull(theInterceptorService, "theInterceptorService must not be null");
735                myInterceptorService = theInterceptorService;
736        }
737
738        /**
739         * Sets (or clears) the list of interceptors
740         *
741         * @param theList The list of interceptors (may be null)
742         * @deprecated As of HAPI FHIR 3.8.0, use {@link #getInterceptorService()} to access the interceptor service. You can register and unregister interceptors using this service.
743         */
744        @Deprecated
745        public void setInterceptors(@Nonnull List<?> theList) {
746                myInterceptorService.unregisterAllInterceptors();
747                myInterceptorService.registerInterceptors(theList);
748        }
749
750        /**
751         * Sets (or clears) the list of interceptors
752         *
753         * @param theInterceptors The list of interceptors (may be null)
754         * @deprecated As of HAPI FHIR 3.8.0, use {@link #getInterceptorService()} to access the interceptor service. You can register and unregister interceptors using this service.
755         */
756        @Deprecated
757        public void setInterceptors(IServerInterceptor... theInterceptors) {
758                Validate.noNullElements(theInterceptors, "theInterceptors must not contain any null elements");
759                setInterceptors(Arrays.asList(theInterceptors));
760        }
761
762        @Override
763        public IPagingProvider getPagingProvider() {
764                return myPagingProvider;
765        }
766
767        /**
768         * Sets the paging provider to use, or <code>null</code> to use no paging (which is the default).
769         * This will set defaultPageSize and maximumPageSize from the paging provider.
770         */
771        public void setPagingProvider(IPagingProvider thePagingProvider) {
772                myPagingProvider = thePagingProvider;
773                if (myPagingProvider != null) {
774                        setDefaultPageSize(myPagingProvider.getDefaultPageSize());
775                        setMaximumPageSize(myPagingProvider.getMaximumPageSize());
776                }
777        }
778
779        @Override
780        public Integer getDefaultPageSize() {
781                return myDefaultPageSize;
782        }
783
784        /**
785         * Sets the default page size to use, or <code>null</code> if no default page size
786         */
787        public void setDefaultPageSize(Integer thePageSize) {
788                myDefaultPageSize = thePageSize;
789        }
790
791        @Override
792        public Integer getMaximumPageSize() {
793                return myMaximumPageSize;
794        }
795
796        /**
797         * Sets the maximum page size to use, or <code>null</code> if no maximum page size
798         */
799        public void setMaximumPageSize(Integer theMaximumPageSize) {
800                myMaximumPageSize = theMaximumPageSize;
801        }
802
803        /**
804         * Provides the non-resource specific providers which implement method calls on this server
805         *
806         * @see #getResourceProviders()
807         */
808        public Collection<Object> getPlainProviders() {
809                return myPlainProviders;
810        }
811
812        /**
813         * Sets the non-resource specific providers which implement method calls on this server.
814         *
815         * @see #setResourceProviders(Collection)
816         * @deprecated This method causes inconsistent behaviour depending on the order it is called in. Use {@link #registerProviders(Object...)} instead.
817         */
818        @Deprecated
819        public void setPlainProviders(Object... theProv) {
820                setPlainProviders(Arrays.asList(theProv));
821        }
822
823        /**
824         * Sets the non-resource specific providers which implement method calls on this server.
825         *
826         * @see #setResourceProviders(Collection)
827         * @deprecated This method causes inconsistent behaviour depending on the order it is called in. Use {@link #registerProviders(Object...)} instead.
828         */
829        @Deprecated
830        public void setPlainProviders(Collection<Object> theProviders) {
831                Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
832
833                myPlainProviders.clear();
834                myPlainProviders.addAll(theProviders);
835        }
836
837        /**
838         * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path
839         * implementation
840         *
841         * @param requestFullPath    the full request path
842         * @param servletContextPath the servelet context path
843         * @param servletPath        the servelet path
844         * @return created resource path
845         */
846        // NOTE: Don't make this a static method!! People want to override it
847        protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {
848                return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath));
849        }
850
851        public Collection<ResourceBinding> getResourceBindings() {
852                return myResourceNameToBinding.values();
853        }
854
855        public Collection<BaseMethodBinding> getProviderMethodBindings(Object theProvider) {
856                Set<BaseMethodBinding> retVal = new HashSet<>();
857                for (ResourceBinding resourceBinding : getResourceBindings()) {
858                        for (BaseMethodBinding methodBinding : resourceBinding.getMethodBindings()) {
859                                if (theProvider.equals(methodBinding.getProvider())) {
860                                        retVal.add(methodBinding);
861                                }
862                        }
863                }
864
865                return retVal;
866        }
867
868        /**
869         * Provides the resource providers for this server
870         */
871        public List<IResourceProvider> getResourceProviders() {
872                return Collections.unmodifiableList(myResourceProviders);
873        }
874
875        /**
876         * Sets the resource providers for this server
877         */
878        public void setResourceProviders(IResourceProvider... theResourceProviders) {
879                myResourceProviders.clear();
880                if (theResourceProviders != null) {
881                        myResourceProviders.addAll(Arrays.asList(theResourceProviders));
882                }
883        }
884
885        /**
886         * Sets the resource providers for this server
887         */
888        public void setResourceProviders(Collection<IResourceProvider> theProviders) {
889                Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
890
891                myResourceProviders.clear();
892                myResourceProviders.addAll(theProviders);
893        }
894
895        /**
896         * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this
897         * server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
898         */
899        public IServerAddressStrategy getServerAddressStrategy() {
900                return myServerAddressStrategy;
901        }
902
903        /**
904         * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this
905         * server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
906         */
907        public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) {
908                Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null");
909                myServerAddressStrategy = theServerAddressStrategy;
910        }
911
912        /**
913         * Returns the server base URL (with no trailing '/') for a given request
914         */
915        public String getServerBaseForRequest(ServletRequestDetails theRequest) {
916                String fhirServerBase;
917                fhirServerBase =
918                                myServerAddressStrategy.determineServerBase(getServletContext(), theRequest.getServletRequest());
919                assert isNotBlank(fhirServerBase) : "Server Address Strategy did not return a value";
920
921                if (fhirServerBase.endsWith("/")) {
922                        fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1);
923                }
924
925                if (myTenantIdentificationStrategy != null) {
926                        fhirServerBase = myTenantIdentificationStrategy.massageServerBaseUrl(fhirServerBase, theRequest);
927                }
928
929                return fhirServerBase;
930        }
931
932        /**
933         * Returns the method bindings for this server which are not specific to any particular resource type. This method is
934         * internal to HAPI and developers generally do not need to interact with it. Use
935         * with caution, as it may change.
936         */
937        public List<BaseMethodBinding> getServerBindings() {
938                return myServerBinding.getMethodBindings();
939        }
940
941        /**
942         * Returns the server conformance provider, which is the provider that is used to generate the server's conformance
943         * (metadata) statement if one has been explicitly defined.
944         * <p>
945         * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or
946         * set to <code>null</code> to use the appropriate one for the given FHIR version.
947         * </p>
948         */
949        public Object getServerConformanceProvider() {
950                return myServerConformanceProvider;
951        }
952
953        /**
954         * Returns the server conformance provider, which is the provider that is used to generate the server's conformance
955         * (metadata) statement.
956         * <p>
957         * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be
958         * changed, or set to <code>null</code> if you do not wish to export a conformance
959         * statement.
960         * </p>
961         * This method should only be called before the server is initialized.
962         * Calling it after the server has started is allowed, but you should be
963         * very careful in this case that you only call it while no traffic is
964         * hitting the server.
965         *
966         * @throws IllegalStateException Note that this method can only be called prior to {@link #init() initialization} and will throw an
967         *                               {@link IllegalStateException} if called after that.
968         */
969        public void setServerConformanceProvider(@Nonnull Object theServerConformanceProvider) {
970                Validate.notNull(theServerConformanceProvider, "theServerConformanceProvider must not be null");
971
972                if (myServerConformanceProvider != null) {
973                        unregisterProvider(myServerConformanceProvider);
974                }
975
976                // call the setRestfulServer() method to point the Conformance
977                // Provider to this server instance. This is done to avoid
978                // passing the server into the constructor. Having that sort
979                // of cross linkage causes reference cycles in Spring wiring
980                try {
981                        Method setRestfulServer =
982                                        theServerConformanceProvider.getClass().getMethod("setRestfulServer", RestfulServer.class);
983                        if (setRestfulServer != null) {
984                                setRestfulServer.invoke(theServerConformanceProvider, this);
985                        }
986                } catch (Exception e) {
987                        ourLog.warn("Error calling IServerConformanceProvider.setRestfulServer", e);
988                }
989                myServerConformanceProvider = theServerConformanceProvider;
990
991                findResourceMethods(myServerConformanceProvider);
992        }
993
994        /**
995         * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only,
996         * but can be helpful to set with something appropriate.
997         *
998         * @see RestfulServer#setServerName(String)
999         */
1000        public String getServerName() {
1001                return myServerName;
1002        }
1003
1004        /**
1005         * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only,
1006         * but can be helpful to set with something appropriate.
1007         */
1008        public void setServerName(String theServerName) {
1009                myServerName = theServerName;
1010        }
1011
1012        /**
1013         * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only,
1014         * but can be helpful to set with something appropriate.
1015         */
1016        public String getServerVersion() {
1017                return myServerVersion;
1018        }
1019
1020        /**
1021         * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only,
1022         * but can be helpful to set with something appropriate.
1023         */
1024        public void setServerVersion(String theServerVersion) {
1025                myServerVersion = theServerVersion;
1026        }
1027
1028        @SuppressWarnings("WeakerAccess")
1029        protected void handleRequest(
1030                        RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse)
1031                        throws ServletException, IOException {
1032                String fhirServerBase;
1033                ServletRequestDetails requestDetails = newRequestDetails(theRequestType, theRequest, theResponse);
1034
1035                String requestId = getOrCreateRequestId(theRequest);
1036                requestDetails.setRequestId(requestId);
1037                addRequestIdToResponse(requestDetails, requestId);
1038
1039                theRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, getServletContext());
1040
1041                // keep track of any unhandled exceptions in case the exception handler throws another exception
1042                Throwable unhandledException = null;
1043
1044                try {
1045
1046                        /* ***********************************
1047                         * Parse out the request parameters
1048                         * ***********************************/
1049
1050                        String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI());
1051                        String servletPath = StringUtils.defaultString(theRequest.getServletPath());
1052                        StringBuffer requestUrl = theRequest.getRequestURL();
1053                        String servletContextPath = myServerAddressStrategy.determineServletContextPath(theRequest, this);
1054
1055                        /*
1056                         * Just for debugging..
1057                         */
1058                        if (ourLog.isTraceEnabled()) {
1059                                ourLog.trace("Request FullPath: {}", requestFullPath);
1060                                ourLog.trace("Servlet Path: {}", servletPath);
1061                                ourLog.trace("Request Url: {}", requestUrl);
1062                                ourLog.trace("Context Path: {}", servletContextPath);
1063                        }
1064
1065                        String completeUrl;
1066                        Map<String, String[]> params = null;
1067                        if (isNotBlank(theRequest.getQueryString())) {
1068                                completeUrl = requestUrl + "?" + theRequest.getQueryString();
1069                                /*
1070                                 * By default, we manually parse the request params (the URL params, or the body for
1071                                 * POST form queries) since Java containers can't be trusted to use UTF-8 encoding
1072                                 * when parsing. Specifically Tomcat 7 and Glassfish 4.0 use 8859-1 for some dumb
1073                                 * reason.... grr.....
1074                                 */
1075                                if (isIgnoreServerParsedRequestParameters()) {
1076                                        String contentType = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
1077                                        if (theRequestType == RequestTypeEnum.POST
1078                                                        && isNotBlank(contentType)
1079                                                        && contentType.startsWith(Constants.CT_X_FORM_URLENCODED)) {
1080                                                String requestBody = toUtf8String(requestDetails.loadRequestContents());
1081                                                params = UrlUtil.parseQueryStrings(theRequest.getQueryString(), requestBody);
1082                                        } else if (theRequestType == RequestTypeEnum.GET) {
1083                                                params = UrlUtil.parseQueryString(theRequest.getQueryString());
1084                                        }
1085                                }
1086                        } else {
1087                                completeUrl = requestUrl.toString();
1088                        }
1089
1090                        if (params == null) {
1091
1092                                // If the request is coming in with a content-encoding, don't try to
1093                                // load the params from the content.
1094                                if (isNotBlank(theRequest.getHeader(Constants.HEADER_CONTENT_ENCODING))) {
1095                                        if (isNotBlank(theRequest.getQueryString())) {
1096                                                params = UrlUtil.parseQueryString(theRequest.getQueryString());
1097                                        } else {
1098                                                params = Collections.emptyMap();
1099                                        }
1100                                }
1101
1102                                if (params == null) {
1103                                        params = new HashMap<>(theRequest.getParameterMap());
1104                                }
1105                        }
1106
1107                        requestDetails.setParameters(params);
1108
1109                        /* *************************
1110                         * Notify interceptors about the incoming request
1111                         * *************************/
1112
1113                        // Interceptor: SERVER_INCOMING_REQUEST_PRE_PROCESSED
1114                        if (myInterceptorService.hasHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED)) {
1115                                HookParams preProcessedParams = new HookParams();
1116                                preProcessedParams.add(HttpServletRequest.class, theRequest);
1117                                preProcessedParams.add(HttpServletResponse.class, theResponse);
1118                                if (!myInterceptorService.callHooks(
1119                                                Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED, preProcessedParams)) {
1120                                        return;
1121                                }
1122                        }
1123
1124                        String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath);
1125
1126                        if (requestPath.length() > 0 && requestPath.charAt(0) == '/') {
1127                                requestPath = requestPath.substring(1);
1128                        }
1129
1130                        IIdType id;
1131                        populateRequestDetailsFromRequestPath(requestDetails, requestPath);
1132
1133                        fhirServerBase = getServerBaseForRequest(requestDetails);
1134
1135                        if (theRequestType == RequestTypeEnum.PUT) {
1136                                String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION);
1137                                if (contentLocation != null) {
1138                                        id = myFhirContext.getVersion().newIdType();
1139                                        id.setValue(contentLocation);
1140                                        requestDetails.setId(id);
1141                                }
1142                        }
1143
1144                        String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING);
1145                        boolean respondGzip = false;
1146                        if (acceptEncoding != null) {
1147                                String[] parts = acceptEncoding.trim().split("\\s*,\\s*");
1148                                for (String string : parts) {
1149                                        if (string.equals("gzip")) {
1150                                                respondGzip = true;
1151                                                break;
1152                                        }
1153                                }
1154                        }
1155                        requestDetails.setRespondGzip(respondGzip);
1156                        requestDetails.setRequestPath(requestPath);
1157                        requestDetails.setFhirServerBase(fhirServerBase);
1158                        requestDetails.setCompleteUrl(completeUrl);
1159
1160                        // Interceptor: SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED
1161                        if (myInterceptorService.hasHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED)) {
1162                                HookParams preProcessedParams = new HookParams();
1163                                preProcessedParams.add(HttpServletRequest.class, theRequest);
1164                                preProcessedParams.add(HttpServletResponse.class, theResponse);
1165                                preProcessedParams.add(RequestDetails.class, requestDetails);
1166                                preProcessedParams.add(ServletRequestDetails.class, requestDetails);
1167                                if (!myInterceptorService.callHooks(
1168                                                Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED, preProcessedParams)) {
1169                                        return;
1170                                }
1171                        }
1172
1173                        validateRequest(requestDetails);
1174
1175                        BaseMethodBinding resourceMethod = determineResourceMethod(requestDetails, requestPath);
1176
1177                        RestOperationTypeEnum operation = resourceMethod.getRestOperationType(requestDetails);
1178                        requestDetails.setRestOperationType(operation);
1179
1180                        // Interceptor: SERVER_INCOMING_REQUEST_POST_PROCESSED
1181                        if (myInterceptorService.hasHooks(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)) {
1182                                HookParams postProcessedParams = new HookParams();
1183                                postProcessedParams.add(RequestDetails.class, requestDetails);
1184                                postProcessedParams.add(ServletRequestDetails.class, requestDetails);
1185                                postProcessedParams.add(HttpServletRequest.class, theRequest);
1186                                postProcessedParams.add(HttpServletResponse.class, theResponse);
1187                                if (!myInterceptorService.callHooks(
1188                                                Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED, postProcessedParams)) {
1189                                        return;
1190                                }
1191                        }
1192
1193                        /*
1194                         * Actually invoke the server method. This call is to a HAPI method binding, which
1195                         * is an object that wraps a specific implementing (user-supplied) method, but
1196                         * handles its input and provides its output back to the client.
1197                         *
1198                         * This is basically the end of processing for a successful request, since the
1199                         * method binding replies to the client and closes the response.
1200                         */
1201                        resourceMethod.invokeServer(this, requestDetails);
1202
1203                        // Invoke interceptors
1204                        HookParams hookParams = new HookParams();
1205                        hookParams.add(RequestDetails.class, requestDetails);
1206                        hookParams.add(ServletRequestDetails.class, requestDetails);
1207                        myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY, hookParams);
1208
1209                } catch (NotModifiedException | AuthenticationException e) {
1210
1211                        unhandledException = e;
1212
1213                        HookParams handleExceptionParams = new HookParams();
1214                        handleExceptionParams.add(RequestDetails.class, requestDetails);
1215                        handleExceptionParams.add(ServletRequestDetails.class, requestDetails);
1216                        handleExceptionParams.add(HttpServletRequest.class, theRequest);
1217                        handleExceptionParams.add(HttpServletResponse.class, theResponse);
1218                        handleExceptionParams.add(BaseServerResponseException.class, e);
1219                        if (!myInterceptorService.callHooks(Pointcut.SERVER_HANDLE_EXCEPTION, handleExceptionParams)) {
1220                                return;
1221                        }
1222
1223                        writeExceptionToResponse(theResponse, e);
1224                        unhandledException = null;
1225
1226                } catch (Throwable e) {
1227
1228                        unhandledException = e;
1229
1230                        /*
1231                         * We have caught an exception during request processing. This might be because a handling method threw
1232                         * something they wanted to throw (e.g. UnprocessableEntityException because the request
1233                         * had business requirement problems) or it could be due to bugs (e.g. NullPointerException).
1234                         *
1235                         * First we let the interceptors have a crack at converting the exception into something HAPI can use
1236                         * (BaseServerResponseException)
1237                         */
1238                        HookParams preProcessParams = new HookParams();
1239                        preProcessParams.add(RequestDetails.class, requestDetails);
1240                        preProcessParams.add(ServletRequestDetails.class, requestDetails);
1241                        preProcessParams.add(HttpServletRequest.class, theRequest);
1242                        preProcessParams.add(HttpServletResponse.class, theResponse);
1243                        preProcessParams.add(Throwable.class, e);
1244                        BaseServerResponseException exception =
1245                                        (BaseServerResponseException) myInterceptorService.callHooksAndReturnObject(
1246                                                        Pointcut.SERVER_PRE_PROCESS_OUTGOING_EXCEPTION, preProcessParams);
1247
1248                        /*
1249                         * If none of the interceptors converted the exception, default behaviour is to keep the exception as-is if it
1250                         * extends BaseServerResponseException, otherwise wrap it in an
1251                         * InternalErrorException.
1252                         */
1253                        if (exception == null) {
1254                                exception = DEFAULT_EXCEPTION_HANDLER.preProcessOutgoingException(requestDetails, e, theRequest);
1255                        }
1256
1257                        /*
1258                         * If it's a 410 Gone, we want to include a location header in the response
1259                         * if we can, since that can include the resource version which is nice
1260                         * for the user.
1261                         */
1262                        if (exception instanceof ResourceGoneException) {
1263                                IIdType resourceId = ((ResourceGoneException) exception).getResourceId();
1264                                if (resourceId != null && resourceId.hasResourceType() && resourceId.hasIdPart()) {
1265                                        String baseUrl =
1266                                                        myServerAddressStrategy.determineServerBase(theRequest.getServletContext(), theRequest);
1267                                        resourceId = resourceId.withServerBase(baseUrl, resourceId.getResourceType());
1268                                        requestDetails.getResponse().addHeader(Constants.HEADER_LOCATION, resourceId.getValue());
1269                                }
1270                        }
1271
1272                        /*
1273                         * Next, interceptors get a shot at handling the exception
1274                         */
1275                        HookParams handleExceptionParams = new HookParams();
1276                        handleExceptionParams.add(RequestDetails.class, requestDetails);
1277                        handleExceptionParams.add(ServletRequestDetails.class, requestDetails);
1278                        handleExceptionParams.add(HttpServletRequest.class, theRequest);
1279                        handleExceptionParams.add(HttpServletResponse.class, theResponse);
1280                        handleExceptionParams.add(BaseServerResponseException.class, exception);
1281                        if (!myInterceptorService.callHooks(Pointcut.SERVER_HANDLE_EXCEPTION, handleExceptionParams)) {
1282                                return;
1283                        }
1284
1285                        /*
1286                         * If we're handling an exception, no summary mode should be applied
1287                         */
1288                        requestDetails.removeParameter(Constants.PARAM_SUMMARY);
1289                        requestDetails.removeParameter(Constants.PARAM_ELEMENTS);
1290                        requestDetails.removeParameter(Constants.PARAM_ELEMENTS + Constants.PARAM_ELEMENTS_EXCLUDE_MODIFIER);
1291
1292                        /*
1293                         * If nobody handles it, default behaviour is to stream back the OperationOutcome to the client.
1294                         */
1295                        DEFAULT_EXCEPTION_HANDLER.handleException(requestDetails, exception, theRequest, theResponse);
1296                        unhandledException = null;
1297
1298                } finally {
1299
1300                        if (unhandledException != null) {
1301                                ourLog.error(
1302                                                Msg.code(2544) + "Exception handling threw an exception.  Initial exception was: {}",
1303                                                unhandledException.getMessage(),
1304                                                unhandledException);
1305                                unhandledException = null;
1306                        }
1307
1308                        HookParams params = new HookParams();
1309                        params.add(RequestDetails.class, requestDetails);
1310                        params.addIfMatchesType(ServletRequestDetails.class, requestDetails);
1311                        myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED, params);
1312                }
1313        }
1314
1315        /**
1316         * Subclasses may override this to customize the way that the RequestDetails object is created. Generally speaking, the
1317         * right way to do this is to override this method, but call the super-implementation (<code>super.newRequestDetails</code>)
1318         * and then customize the returned object before returning it.
1319         *
1320         * @param theRequestType The HTTP request verb
1321         * @param theRequest     The servlet request
1322         * @param theResponse    The servlet response
1323         * @return A ServletRequestDetails instance to be passed to any resource providers, interceptors, etc. that are invoked as a part of serving this request.
1324         */
1325        @Nonnull
1326        protected ServletRequestDetails newRequestDetails(
1327                        RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) {
1328                ServletRequestDetails requestDetails = newRequestDetails();
1329                requestDetails.setServer(this);
1330                requestDetails.setRequestType(theRequestType);
1331                requestDetails.setServletRequest(theRequest);
1332                requestDetails.setServletResponse(theResponse);
1333                return requestDetails;
1334        }
1335
1336        /**
1337         * @deprecated Deprecated in HAPI FHIR 4.1.0 - Users wishing to override this method should override {@link #newRequestDetails(RequestTypeEnum, HttpServletRequest, HttpServletResponse)} instead
1338         */
1339        @Deprecated
1340        protected ServletRequestDetails newRequestDetails() {
1341                return new ServletRequestDetails(getInterceptorService());
1342        }
1343
1344        protected void addRequestIdToResponse(ServletRequestDetails theRequestDetails, String theRequestId) {
1345                String caseSensitiveRequestIdKey = Constants.HEADER_REQUEST_ID;
1346                for (String key : theRequestDetails.getHeaders().keySet()) {
1347                        if (Constants.HEADER_REQUEST_ID.equalsIgnoreCase(key)) {
1348                                caseSensitiveRequestIdKey = key;
1349                                break;
1350                        }
1351                }
1352                theRequestDetails.getResponse().addHeader(caseSensitiveRequestIdKey, theRequestId);
1353        }
1354
1355        /**
1356         * Reads a request ID from the request headers via the {@link Constants#HEADER_REQUEST_ID}
1357         * header, or generates one if none is supplied.
1358         * <p>
1359         * Note that the generated request ID is a random 64-bit long integer encoded as
1360         * hexadecimal. It is not generated using any cryptographic algorithms or a secure
1361         * PRNG, so it should not be used for anything other than troubleshooting purposes.
1362         * </p>
1363         */
1364        protected String getOrCreateRequestId(HttpServletRequest theRequest) {
1365                String requestId = ServletRequestTracing.maybeGetRequestId(theRequest);
1366
1367                // TODO can we delete this and newRequestId()
1368                //  and use ServletRequestTracing.getOrGenerateRequestId() instead?
1369                //  newRequestId() is protected.  Do you think anyone actually overrode it?
1370                if (isBlank(requestId)) {
1371                        int requestIdLength = Constants.REQUEST_ID_LENGTH;
1372                        requestId = newRequestId(requestIdLength);
1373                }
1374
1375                return requestId;
1376        }
1377
1378        /**
1379         * Generate a new request ID string. Subclasses may ovrride.
1380         */
1381        protected String newRequestId(int theRequestIdLength) {
1382                String requestId;
1383                requestId = RandomStringUtils.randomAlphanumeric(theRequestIdLength);
1384                return requestId;
1385        }
1386
1387        protected void validateRequest(ServletRequestDetails theRequestDetails) {
1388                String[] elements = theRequestDetails.getParameters().get(Constants.PARAM_ELEMENTS);
1389                if (elements != null) {
1390                        for (String next : elements) {
1391                                if (next.indexOf(':') != -1) {
1392                                        throw new InvalidRequestException(Msg.code(295) + "Invalid _elements value: \"" + next + "\"");
1393                                }
1394                        }
1395                }
1396
1397                elements = theRequestDetails
1398                                .getParameters()
1399                                .get(Constants.PARAM_ELEMENTS + Constants.PARAM_ELEMENTS_EXCLUDE_MODIFIER);
1400                if (elements != null) {
1401                        for (String next : elements) {
1402                                if (next.indexOf(':') != -1) {
1403                                        throw new InvalidRequestException(Msg.code(296) + "Invalid _elements value: \"" + next + "\"");
1404                                }
1405                        }
1406                }
1407        }
1408
1409        /**
1410         * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations,
1411         * but subclasses may put initialization code in {@link #initialize()}, which is
1412         * called immediately before beginning initialization of the restful server's internal init.
1413         */
1414        @Override
1415        public final void init() throws ServletException {
1416                myProviderRegistrationMutex.lock();
1417                try {
1418                        initialize();
1419
1420                        Object confProvider;
1421                        try {
1422                                ourLog.info("Initializing HAPI FHIR restful server running in "
1423                                                + getFhirContext().getVersion().getVersion().name() + " mode");
1424
1425                                Collection<IResourceProvider> resourceProvider = getResourceProviders();
1426                                // 'true' tells registerProviders() that
1427                                // this call is part of initialization
1428                                registerProviders(resourceProvider, true);
1429
1430                                Collection<Object> providers = getPlainProviders();
1431                                // 'true' tells registerProviders() that
1432                                // this call is part of initialization
1433                                registerProviders(providers, true);
1434
1435                                confProvider = getServerConformanceProvider();
1436                                if (confProvider == null) {
1437                                        IFhirVersionServer versionServer =
1438                                                        (IFhirVersionServer) getFhirContext().getVersion().getServerVersion();
1439                                        confProvider = versionServer.createServerConformanceProvider(this);
1440                                }
1441                                setServerConformanceProvider(confProvider);
1442
1443                                ourLog.trace("Invoking provider initialize methods");
1444                                if (getResourceProviders() != null) {
1445                                        for (IResourceProvider iResourceProvider : getResourceProviders()) {
1446                                                invokeInitialize(iResourceProvider);
1447                                        }
1448                                }
1449
1450                                invokeInitialize(confProvider);
1451                                if (getPlainProviders() != null) {
1452                                        for (Object next : getPlainProviders()) {
1453                                                invokeInitialize(next);
1454                                        }
1455                                }
1456
1457                                /*
1458                                 * This is a bit odd, but we have a placeholder @GetPage method for now
1459                                 * that gets the server to bind for the paging request. At some point
1460                                 * it would be nice to set things up so that client code could provide
1461                                 * an alternate implementation, but this isn't currently possible..
1462                                 */
1463                                findResourceMethods(new PageProvider());
1464
1465                        } catch (Exception e) {
1466                                ourLog.error("An error occurred while loading request handlers!", e);
1467                                throw new ServletException(
1468                                                Msg.code(297) + "Failed to initialize FHIR Restful server: " + e.getMessage(), e);
1469                        }
1470
1471                        myStarted = true;
1472                        ourLog.info("A FHIR has been lit on this server");
1473                } finally {
1474                        myProviderRegistrationMutex.unlock();
1475                }
1476        }
1477
1478        /**
1479         * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the
1480         * server being used.
1481         *
1482         * @throws ServletException If the initialization failed. Note that you should consider throwing {@link UnavailableException}
1483         *                          (which extends {@link ServletException}), as this is a flag to the servlet container
1484         *                          that the servlet is not usable.
1485         */
1486        protected void initialize() throws ServletException {
1487                // nothing by default
1488        }
1489
1490        private void invokeDestroy(Object theProvider) {
1491                invokeDestroy(theProvider, theProvider.getClass());
1492        }
1493
1494        private void invokeDestroy(Object theProvider, Class<?> clazz) {
1495                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
1496                        Destroy destroy = m.getAnnotation(Destroy.class);
1497                        if (destroy != null) {
1498                                invokeInitializeOrDestroyMethod(theProvider, m, "destroy");
1499                        }
1500                }
1501
1502                Class<?> supertype = clazz.getSuperclass();
1503                if (!Object.class.equals(supertype)) {
1504                        invokeDestroy(theProvider, supertype);
1505                }
1506        }
1507
1508        private void invokeInitialize(Object theProvider) {
1509                invokeInitialize(theProvider, theProvider.getClass());
1510        }
1511
1512        private void invokeInitialize(Object theProvider, Class<?> clazz) {
1513                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
1514                        Initialize initialize = m.getAnnotation(Initialize.class);
1515                        if (initialize != null) {
1516                                invokeInitializeOrDestroyMethod(theProvider, m, "initialize");
1517                        }
1518                }
1519
1520                Class<?> supertype = clazz.getSuperclass();
1521                if (!Object.class.equals(supertype)) {
1522                        invokeInitialize(theProvider, supertype);
1523                }
1524        }
1525
1526        private void invokeInitializeOrDestroyMethod(Object theProvider, Method m, String theMethodDescription) {
1527
1528                Class<?>[] paramTypes = m.getParameterTypes();
1529                Object[] params = new Object[paramTypes.length];
1530
1531                int index = 0;
1532                for (Class<?> nextParamType : paramTypes) {
1533
1534                        if (RestfulServer.class.equals(nextParamType) || IRestfulServerDefaults.class.equals(nextParamType)) {
1535                                params[index] = this;
1536                        }
1537
1538                        index++;
1539                }
1540
1541                try {
1542                        m.invoke(theProvider, params);
1543                } catch (Exception e) {
1544                        ourLog.error("Exception occurred in " + theMethodDescription + " method '" + m.getName() + "'", e);
1545                }
1546        }
1547
1548        /**
1549         * Should the server "pretty print" responses by default (requesting clients can always override this default by
1550         * supplying an <code>Accept</code> header in the request, or a <code>_pretty</code>
1551         * parameter in the request URL.
1552         * <p>
1553         * The default is <code>false</code>
1554         * </p>
1555         * <p>
1556         * Note that this setting is ignored by {@link ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor}
1557         * when streaming HTML, although even when that interceptor it used this setting will
1558         * still be honoured when streaming raw FHIR.
1559         * </p>
1560         *
1561         * @return Returns the default pretty print setting
1562         */
1563        @Override
1564        public boolean isDefaultPrettyPrint() {
1565                return myDefaultPrettyPrint;
1566        }
1567
1568        /**
1569         * Should the server "pretty print" responses by default (requesting clients can always override this default by
1570         * supplying an <code>Accept</code> header in the request, or a <code>_pretty</code>
1571         * parameter in the request URL.
1572         * <p>
1573         * The default is <code>false</code>
1574         * </p>
1575         * <p>
1576         * Note that this setting is ignored by {@link ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor}
1577         * when streaming HTML, although even when that interceptor it used this setting will
1578         * still be honoured when streaming raw FHIR.
1579         * </p>
1580         *
1581         * @param theDefaultPrettyPrint The default pretty print setting
1582         */
1583        public void setDefaultPrettyPrint(boolean theDefaultPrettyPrint) {
1584                myDefaultPrettyPrint = theDefaultPrettyPrint;
1585        }
1586
1587        /**
1588         * If set to <code>true</code> (the default is <code>true</code>) this server will not
1589         * use the parsed request parameters (URL parameters and HTTP POST form contents) but
1590         * will instead parse these values manually from the request URL and request body.
1591         * <p>
1592         * This is useful because many servlet containers (e.g. Tomcat, Glassfish) will use
1593         * ISO-8859-1 encoding to parse escaped URL characters instead of using UTF-8
1594         * as is specified by FHIR.
1595         * </p>
1596         */
1597        public boolean isIgnoreServerParsedRequestParameters() {
1598                return myIgnoreServerParsedRequestParameters;
1599        }
1600
1601        /**
1602         * If set to <code>true</code> (the default is <code>true</code>) this server will not
1603         * use the parsed request parameters (URL parameters and HTTP POST form contents) but
1604         * will instead parse these values manually from the request URL and request body.
1605         * <p>
1606         * This is useful because many servlet containers (e.g. Tomcat, Glassfish) will use
1607         * ISO-8859-1 encoding to parse escaped URL characters instead of using UTF-8
1608         * as is specified by FHIR.
1609         * </p>
1610         */
1611        public void setIgnoreServerParsedRequestParameters(boolean theIgnoreServerParsedRequestParameters) {
1612                myIgnoreServerParsedRequestParameters = theIgnoreServerParsedRequestParameters;
1613        }
1614
1615        /**
1616         * Should the server attempt to decompress incoming request contents (default is <code>true</code>). Typically this
1617         * should be set to <code>true</code> unless the server has other configuration to
1618         * deal with decompressing request bodies (e.g. a filter applied to the whole server).
1619         */
1620        public boolean isUncompressIncomingContents() {
1621                return myUncompressIncomingContents;
1622        }
1623
1624        /**
1625         * Should the server attempt to decompress incoming request contents (default is <code>true</code>). Typically this
1626         * should be set to <code>true</code> unless the server has other configuration to
1627         * deal with decompressing request bodies (e.g. a filter applied to the whole server).
1628         */
1629        public void setUncompressIncomingContents(boolean theUncompressIncomingContents) {
1630                myUncompressIncomingContents = theUncompressIncomingContents;
1631        }
1632
1633        private String resolveRequestPath(RequestDetails theRequestDetails, String theRequestPath) {
1634                if (myTenantIdentificationStrategy != null) {
1635                        theRequestPath = myTenantIdentificationStrategy.resolveRelativeUrl(theRequestPath, theRequestDetails);
1636                }
1637                return theRequestPath;
1638        }
1639
1640        public void populateRequestDetailsFromRequestPath(RequestDetails theRequestDetails, String theRequestPath) {
1641                String resolvedRequestPath = resolveRequestPath(theRequestDetails, theRequestPath);
1642                UrlPathTokenizer tok = new UrlPathTokenizer(resolvedRequestPath);
1643
1644                if (myTenantIdentificationStrategy != null) {
1645                        myTenantIdentificationStrategy.extractTenant(tok, theRequestDetails);
1646                }
1647
1648                IIdType id = null;
1649                String operation = null;
1650                String compartment = null;
1651                String resourceName = null;
1652                if (tok.hasMoreTokens()) {
1653                        resourceName = tok.nextTokenUnescapedAndSanitized();
1654                        if (partIsOperation(resourceName)) {
1655                                operation = resourceName;
1656                                resourceName = null;
1657                        }
1658                }
1659                theRequestDetails.setResourceName(resourceName);
1660
1661                if (tok.hasMoreTokens()) {
1662                        String nextString = tok.nextTokenUnescapedAndSanitized();
1663                        if (partIsOperation(nextString)) {
1664                                operation = nextString;
1665                        } else {
1666                                id = myFhirContext.getVersion().newIdType();
1667                                id.setParts(null, resourceName, UrlUtil.unescape(nextString), null);
1668                        }
1669                }
1670
1671                if (tok.hasMoreTokens()) {
1672                        String nextString = tok.nextTokenUnescapedAndSanitized();
1673                        if (nextString.equals(Constants.PARAM_HISTORY)) {
1674                                if (tok.hasMoreTokens()) {
1675                                        String versionString = tok.nextTokenUnescapedAndSanitized();
1676                                        if (id == null) {
1677                                                throw new InvalidRequestException(
1678                                                                Msg.code(298) + "Don't know how to handle request path: " + resolvedRequestPath);
1679                                        }
1680                                        id.setParts(null, resourceName, id.getIdPart(), UrlUtil.unescape(versionString));
1681                                } else {
1682                                        operation = Constants.PARAM_HISTORY;
1683                                }
1684                        } else if (partIsOperation(nextString)) {
1685                                if (operation != null) {
1686                                        throw new InvalidRequestException(
1687                                                        Msg.code(299) + "URL Path contains two operations: " + resolvedRequestPath);
1688                                }
1689                                operation = nextString;
1690                        } else {
1691                                compartment = nextString;
1692                        }
1693                }
1694
1695                // Secondary is for things like ..../_tags/_delete
1696                String secondaryOperation = null;
1697
1698                while (tok.hasMoreTokens()) {
1699                        String nextString = tok.nextTokenUnescapedAndSanitized();
1700                        if (operation == null) {
1701                                operation = nextString;
1702                        } else if (secondaryOperation == null) {
1703                                secondaryOperation = nextString;
1704                        } else {
1705                                throw new InvalidRequestException(Msg.code(300) + "URL path has unexpected token '" + nextString
1706                                                + "' at the end: " + resolvedRequestPath);
1707                        }
1708                }
1709
1710                theRequestDetails.setId(id);
1711                theRequestDetails.setOperation(operation);
1712                theRequestDetails.setSecondaryOperation(secondaryOperation);
1713                theRequestDetails.setCompartmentName(compartment);
1714        }
1715
1716        /**
1717         * Registers an interceptor. This method is a convenience method which calls
1718         * <code>getInterceptorService().registerInterceptor(theInterceptor);</code>
1719         *
1720         * @param theInterceptor The interceptor, must not be null
1721         */
1722        public void registerInterceptor(Object theInterceptor) {
1723                Validate.notNull(theInterceptor, "Interceptor can not be null");
1724                getInterceptorService().registerInterceptor(theInterceptor);
1725        }
1726
1727        /**
1728         * Register a single provider. This could be a Resource Provider or a "plain" provider not associated with any
1729         * resource.
1730         */
1731        public void registerProvider(Object provider) {
1732                if (provider != null) {
1733                        Collection<Object> providerList = new ArrayList<>(1);
1734                        providerList.add(provider);
1735                        registerProviders(providerList);
1736                }
1737        }
1738
1739        /**
1740         * Register a group of providers. These could be Resource Providers (classes implementing {@link IResourceProvider}) or "plain" providers, or a mixture of the two.
1741         *
1742         * @param theProviders a {@code Collection} of theProviders. The parameter could be null or an empty {@code Collection}
1743         */
1744        public void registerProviders(Object... theProviders) {
1745                Validate.noNullElements(theProviders);
1746                registerProviders(Arrays.asList(theProviders));
1747        }
1748
1749        /**
1750         * Register a group of theProviders. These could be Resource Providers, "plain" theProviders or a mixture of the two.
1751         *
1752         * @param theProviders a {@code Collection} of theProviders. The parameter could be null or an empty {@code Collection}
1753         */
1754        public void registerProviders(Collection<?> theProviders) {
1755                Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
1756
1757                myProviderRegistrationMutex.lock();
1758                try {
1759                        if (!myStarted) {
1760                                for (Object provider : theProviders) {
1761                                        ourLog.debug("Registration of provider ["
1762                                                        + provider.getClass().getName() + "] will be delayed until FHIR server startup");
1763                                        if (provider instanceof IResourceProvider) {
1764                                                myResourceProviders.add((IResourceProvider) provider);
1765                                        } else {
1766                                                myPlainProviders.add(provider);
1767                                        }
1768                                }
1769                                return;
1770                        }
1771                } finally {
1772                        myProviderRegistrationMutex.unlock();
1773                }
1774                registerProviders(theProviders, false);
1775        }
1776
1777        /*
1778         * Inner method to actually register theProviders
1779         */
1780        protected void registerProviders(@Nullable Collection<?> theProviders, boolean inInit) {
1781                Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
1782
1783                List<IResourceProvider> newResourceProviders = new ArrayList<>();
1784                List<Object> newPlainProviders = new ArrayList<>();
1785
1786                if (theProviders != null) {
1787                        for (Object provider : theProviders) {
1788                                if (provider instanceof IResourceProvider) {
1789                                        IResourceProvider rsrcProvider = (IResourceProvider) provider;
1790                                        Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType();
1791                                        if (resourceType == null) {
1792                                                throw new NullPointerException(Msg.code(301) + "getResourceType() on class '"
1793                                                                + rsrcProvider.getClass().getCanonicalName() + "' returned null");
1794                                        }
1795                                        if (!inInit) {
1796                                                myResourceProviders.add(rsrcProvider);
1797                                        }
1798                                        newResourceProviders.add(rsrcProvider);
1799                                } else {
1800                                        if (!inInit) {
1801                                                myPlainProviders.add(provider);
1802                                        }
1803                                        newPlainProviders.add(provider);
1804                                }
1805                        }
1806                        if (!newResourceProviders.isEmpty()) {
1807                                ourLog.info(
1808                                                "Added {} resource provider(s). Total {}",
1809                                                newResourceProviders.size(),
1810                                                myResourceProviders.size());
1811                                for (IResourceProvider provider : newResourceProviders) {
1812                                        findResourceMethods(provider);
1813                                }
1814                        }
1815                        if (!newPlainProviders.isEmpty()) {
1816                                ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size(), myPlainProviders.size());
1817                                for (Object provider : newPlainProviders) {
1818                                        findResourceMethods(provider);
1819                                }
1820                        }
1821                        if (!inInit) {
1822                                ourLog.trace("Invoking provider initialize methods");
1823                                if (!newResourceProviders.isEmpty()) {
1824                                        for (IResourceProvider provider : newResourceProviders) {
1825                                                invokeInitialize(provider);
1826                                        }
1827                                }
1828                                if (!newPlainProviders.isEmpty()) {
1829                                        for (Object provider : newPlainProviders) {
1830                                                invokeInitialize(provider);
1831                                        }
1832                                }
1833                        }
1834                }
1835        }
1836
1837        /*
1838         * Remove registered RESTful methods for a Provider (and all superclasses) when it is being unregistered
1839         */
1840        private void removeResourceMethods(Object theProvider) {
1841                ourLog.info("Removing RESTful methods for: {}", theProvider.getClass());
1842                Class<?> clazz = theProvider.getClass();
1843                Class<?> supertype = clazz.getSuperclass();
1844                Collection<String> resourceNames = new ArrayList<>();
1845                while (!Object.class.equals(supertype)) {
1846                        removeResourceMethods(theProvider, supertype, resourceNames);
1847                        removeResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces(), resourceNames);
1848                        supertype = supertype.getSuperclass();
1849                }
1850                removeResourceMethods(theProvider, clazz, resourceNames);
1851                removeResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces(), resourceNames);
1852                removeResourceNameBindings(resourceNames, theProvider);
1853        }
1854
1855        private void removeResourceNameBindings(Collection<String> resourceNames, Object theProvider) {
1856                for (String resourceName : resourceNames) {
1857                        ResourceBinding resourceBinding = myResourceNameToBinding.get(resourceName);
1858                        if (resourceBinding == null) {
1859                                continue;
1860                        }
1861
1862                        for (Iterator<BaseMethodBinding> it =
1863                                                        resourceBinding.getMethodBindings().iterator();
1864                                        it.hasNext(); ) {
1865                                BaseMethodBinding binding = it.next();
1866                                if (theProvider.equals(binding.getProvider())) {
1867                                        it.remove();
1868                                        ourLog.info("{} binding of {} was removed", resourceName, binding);
1869                                }
1870                        }
1871
1872                        if (resourceBinding.getMethodBindings().isEmpty()) {
1873                                myResourceNameToBinding.remove(resourceName);
1874                        }
1875                }
1876        }
1877
1878        private void removeResourceMethodsOnInterfaces(
1879                        Object theProvider, Class<?>[] interfaces, Collection<String> resourceNames) {
1880                for (Class<?> anInterface : interfaces) {
1881                        removeResourceMethods(theProvider, anInterface, resourceNames);
1882                        removeResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces(), resourceNames);
1883                }
1884        }
1885
1886        /*
1887         * Collect the set of RESTful methods for a single class when it is being unregistered
1888         */
1889        private void removeResourceMethods(Object theProvider, Class<?> clazz, Collection<String> resourceNames)
1890                        throws ConfigurationException {
1891                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
1892                        BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
1893                        if (foundMethodBinding == null) {
1894                                continue; // not a bound method
1895                        }
1896                        if (foundMethodBinding instanceof ConformanceMethodBinding) {
1897                                myServerConformanceMethod = null;
1898                                continue;
1899                        }
1900                        String resourceName = foundMethodBinding.getResourceName();
1901                        if (!resourceNames.contains(resourceName)) {
1902                                resourceNames.add(resourceName);
1903                        }
1904                }
1905        }
1906
1907        public Object returnResponse(
1908                        ServletRequestDetails theRequest,
1909                        BaseParseAction<?> outcome,
1910                        int operationStatus,
1911                        boolean allowPrefer,
1912                        MethodOutcome response,
1913                        String resourceName)
1914                        throws IOException {
1915                HttpServletResponse servletResponse = theRequest.getServletResponse();
1916                servletResponse.setStatus(operationStatus);
1917                servletResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8);
1918                addHeadersToResponse(servletResponse);
1919                if (allowPrefer) {
1920                        addContentLocationHeaders(theRequest, servletResponse, response, resourceName);
1921                }
1922                Writer writer;
1923                if (outcome != null) {
1924                        ResponseEncoding encoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequest);
1925                        servletResponse.setContentType(encoding.getResourceContentType());
1926                        writer = servletResponse.getWriter();
1927                        IParser parser = encoding.getEncoding().newParser(getFhirContext());
1928                        parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(this, theRequest));
1929                        outcome.execute(parser, writer);
1930                } else {
1931                        servletResponse.setContentType(Constants.CT_TEXT_WITH_UTF8);
1932                        writer = servletResponse.getWriter();
1933                }
1934                return writer;
1935        }
1936
1937        @Override
1938        protected void service(HttpServletRequest theReq, HttpServletResponse theResp)
1939                        throws ServletException, IOException {
1940                theReq.setAttribute(REQUEST_START_TIME, new Date());
1941
1942                RequestTypeEnum method;
1943                try {
1944                        method = RequestTypeEnum.valueOf(theReq.getMethod());
1945                } catch (IllegalArgumentException e) {
1946                        super.service(theReq, theResp);
1947                        return;
1948                }
1949
1950                switch (method) {
1951                        case DELETE:
1952                                doDelete(theReq, theResp);
1953                                break;
1954                        case GET:
1955                                doGet(theReq, theResp);
1956                                break;
1957                        case OPTIONS:
1958                                doOptions(theReq, theResp);
1959                                break;
1960                        case POST:
1961                                doPost(theReq, theResp);
1962                                break;
1963                        case PUT:
1964                                doPut(theReq, theResp);
1965                                break;
1966                        case PATCH:
1967                        case TRACE:
1968                        case TRACK:
1969                        case HEAD:
1970                        case CONNECT:
1971                        default:
1972                                handleRequest(method, theReq, theResp);
1973                                break;
1974                }
1975        }
1976
1977        /**
1978         * Sets the non-resource specific providers which implement method calls on this server
1979         *
1980         * @see #setResourceProviders(Collection)
1981         */
1982        public void setProviders(Object... theProviders) {
1983                Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
1984
1985                myPlainProviders.clear();
1986                if (theProviders != null) {
1987                        myPlainProviders.addAll(Arrays.asList(theProviders));
1988                }
1989        }
1990
1991        /**
1992         * If provided (default is <code>null</code>), the tenant identification
1993         * strategy provides a mechanism for a multitenant server to identify which tenant
1994         * a given request corresponds to.
1995         */
1996        public void setTenantIdentificationStrategy(ITenantIdentificationStrategy theTenantIdentificationStrategy) {
1997                myTenantIdentificationStrategy = theTenantIdentificationStrategy;
1998        }
1999
2000        protected void throwUnknownFhirOperationException(
2001                        RequestDetails requestDetails, String requestPath, RequestTypeEnum theRequestType) {
2002                FhirContext fhirContext = myFhirContext;
2003                throwUnknownFhirOperationException(requestDetails, requestPath, theRequestType, fhirContext);
2004        }
2005
2006        protected void throwUnknownResourceTypeException(String theResourceName) {
2007                /* perform a 'distinct' in case there are multiple concrete IResourceProviders declared for the same FHIR-Resource. (A concrete IResourceProvider for Patient@Read and a separate concrete for Patient@Search for example */
2008                /* perform a 'sort' to provide an easier to read alphabetized list (vs how the different FHIR-resource IResourceProviders happened to be registered */
2009                List<String> knownDistinctAndSortedResourceTypes = myResourceProviders.stream()
2010                                .map(t ->
2011                                                myFhirContext.getResourceDefinition(t.getResourceType()).getName())
2012                                .distinct()
2013                                .sorted()
2014                                .collect(toList());
2015                throw new ResourceNotFoundException(Msg.code(302) + "Unknown resource type '" + theResourceName
2016                                + "' - Server knows how to handle: " + knownDistinctAndSortedResourceTypes);
2017        }
2018
2019        /**
2020         * Unregisters an interceptor. This method is a convenience method which calls
2021         * <code>getInterceptorService().unregisterInterceptor(theInterceptor);</code>
2022         *
2023         * @param theInterceptor The interceptor, must not be null
2024         */
2025        public void unregisterInterceptor(Object theInterceptor) {
2026                Validate.notNull(theInterceptor, "Interceptor can not be null");
2027                getInterceptorService().unregisterInterceptor(theInterceptor);
2028        }
2029
2030        /**
2031         * Unregister one provider (either a Resource provider or a plain provider)
2032         */
2033        public void unregisterProvider(Object provider) {
2034                if (provider != null) {
2035                        Collection<Object> providerList = new ArrayList<>(1);
2036                        providerList.add(provider);
2037                        unregisterProviders(providerList);
2038                }
2039        }
2040
2041        /**
2042         * Unregister a {@code Collection} of providers
2043         */
2044        public void unregisterProviders(Collection<?> providers) {
2045                if (providers != null) {
2046                        for (Object provider : providers) {
2047                                removeResourceMethods(provider);
2048                                if (provider instanceof IResourceProvider) {
2049                                        myResourceProviders.remove(provider);
2050                                } else {
2051                                        myPlainProviders.remove(provider);
2052                                }
2053                                invokeDestroy(provider);
2054                        }
2055                }
2056        }
2057
2058        /**
2059         * Unregisters all plain and resource providers (but not the conformance provider).
2060         */
2061        public void unregisterAllProviders() {
2062                unregisterAllProviders(myPlainProviders);
2063                unregisterAllProviders(myResourceProviders);
2064        }
2065
2066        private void unregisterAllProviders(List<?> theProviders) {
2067                while (theProviders.size() > 0) {
2068                        unregisterProvider(theProviders.get(0));
2069                }
2070        }
2071
2072        private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException)
2073                        throws IOException {
2074                theResponse.setStatus(theException.getStatusCode());
2075                addHeadersToResponse(theResponse);
2076                if (theException.hasResponseHeaders()) {
2077                        for (Entry<String, List<String>> nextEntry :
2078                                        theException.getResponseHeaders().entrySet()) {
2079                                for (String nextValue : nextEntry.getValue()) {
2080                                        if (isNotBlank(nextValue)) {
2081                                                theResponse.addHeader(nextEntry.getKey(), nextValue);
2082                                        }
2083                                }
2084                        }
2085                }
2086                theResponse.setContentType("text/plain");
2087                theResponse.setCharacterEncoding("UTF-8");
2088                String message = UrlUtil.sanitizeUrlPart(theException.getMessage());
2089                theResponse.getWriter().write(message);
2090        }
2091
2092        /**
2093         * By default, server create/update/patch/transaction methods return a copy of the resource
2094         * as it was stored. This may be overridden by the client using the
2095         * <code>Prefer</code> header.
2096         * <p>
2097         * This setting changes the default behaviour if no Prefer header is supplied by the client.
2098         * The default is {@link PreferReturnEnum#REPRESENTATION}
2099         * </p>
2100         *
2101         * @see <a href="http://hl7.org/fhir/http.html#ops">HL7 FHIR Specification</a> section on the Prefer header
2102         */
2103        @Override
2104        public PreferReturnEnum getDefaultPreferReturn() {
2105                return myDefaultPreferReturn;
2106        }
2107
2108        /**
2109         * By default, server create/update/patch/transaction methods return a copy of the resource
2110         * as it was stored. This may be overridden by the client using the
2111         * <code>Prefer</code> header.
2112         * <p>
2113         * This setting changes the default behaviour if no Prefer header is supplied by the client.
2114         * The default is {@link PreferReturnEnum#REPRESENTATION}
2115         * </p>
2116         *
2117         * @see <a href="http://hl7.org/fhir/http.html#ops">HL7 FHIR Specification</a> section on the Prefer header
2118         */
2119        public void setDefaultPreferReturn(PreferReturnEnum theDefaultPreferReturn) {
2120                Validate.notNull(theDefaultPreferReturn, "theDefaultPreferReturn must not be null");
2121                myDefaultPreferReturn = theDefaultPreferReturn;
2122        }
2123
2124        /**
2125         * Create a CapabilityStatement based on the given request
2126         */
2127        public IBaseConformance getCapabilityStatement(ServletRequestDetails theRequestDetails) {
2128                // Create a cloned request details so we can make it indicate that this is a capabilities request
2129                ServletRequestDetails requestDetails = new ServletRequestDetails(theRequestDetails);
2130                requestDetails.setRestOperationType(RestOperationTypeEnum.METADATA);
2131
2132                return myServerConformanceMethod.provideCapabilityStatement(this, requestDetails);
2133        }
2134
2135        /**
2136         * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
2137         */
2138        protected static int escapedLength(String theServletPath) {
2139                int delta = 0;
2140                for (int i = 0; i < theServletPath.length(); i++) {
2141                        char next = theServletPath.charAt(i);
2142                        if (next == ' ') {
2143                                delta = delta + 2;
2144                        }
2145                }
2146                return theServletPath.length() + delta;
2147        }
2148
2149        public static void throwUnknownFhirOperationException(
2150                        RequestDetails requestDetails,
2151                        String requestPath,
2152                        RequestTypeEnum theRequestType,
2153                        FhirContext theFhirContext) {
2154                String message = theFhirContext
2155                                .getLocalizer()
2156                                .getMessage(
2157                                                RestfulServer.class,
2158                                                "unknownMethod",
2159                                                theRequestType.name(),
2160                                                requestPath,
2161                                                requestDetails.getParameters().keySet());
2162
2163                IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(theFhirContext);
2164                OperationOutcomeUtil.addIssue(theFhirContext, oo, "error", message, null, "not-supported");
2165
2166                throw new InvalidRequestException(Msg.code(303) + message, oo);
2167        }
2168
2169        private static boolean partIsOperation(String nextString) {
2170                return nextString.length() > 0
2171                                && (nextString.charAt(0) == '_'
2172                                                || nextString.charAt(0) == '$'
2173                                                || nextString.equals(Constants.URL_TOKEN_METADATA));
2174        }
2175
2176        //      /**
2177        //       * Returns the read method binding for the given resource type, or
2178        //       * returns <code>null</code> if not
2179        //       * @param theResourceType The resource type, e.g. "Patient"
2180        //       * @return The read method binding, or null
2181        //       */
2182        //      public ReadMethodBinding findReadMethodBinding(String theResourceType) {
2183        //              ReadMethodBinding retVal = null;
2184        //
2185        //              ResourceBinding type = myResourceNameToBinding.get(theResourceType);
2186        //              if (type != null) {
2187        //                      for (BaseMethodBinding<?> next : type.getMethodBindings()) {
2188        //                              if (next instanceof ReadMethodBinding) {
2189        //                                      retVal = (ReadMethodBinding) next;
2190        //                              }
2191        //                      }
2192        //              }
2193        //
2194        //              return retVal;
2195        //      }
2196}