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