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