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