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