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