001package ca.uhn.fhir.rest.server;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 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 */
022import static org.apache.commons.lang3.StringUtils.isBlank;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.Writer;
027import java.lang.annotation.Annotation;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Method;
030import java.lang.reflect.Modifier;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.HashMap;
036import java.util.List;
037import java.util.Map;
038import java.util.StringTokenizer;
039import java.util.concurrent.locks.Lock;
040import java.util.concurrent.locks.ReentrantLock;
041import java.util.jar.Manifest;
042
043import javax.servlet.ServletException;
044import javax.servlet.UnavailableException;
045import javax.servlet.http.HttpServlet;
046import javax.servlet.http.HttpServletRequest;
047import javax.servlet.http.HttpServletResponse;
048
049import org.apache.commons.io.IOUtils;
050import org.apache.commons.lang3.StringUtils;
051import org.apache.commons.lang3.Validate;
052import org.hl7.fhir.instance.model.api.IBaseResource;
053import org.hl7.fhir.instance.model.api.IIdType;
054
055import ca.uhn.fhir.context.ConfigurationException;
056import ca.uhn.fhir.context.FhirContext;
057import ca.uhn.fhir.context.ProvidedResourceScanner;
058import ca.uhn.fhir.context.RuntimeResourceDefinition;
059import ca.uhn.fhir.parser.IParser;
060import ca.uhn.fhir.rest.annotation.Destroy;
061import ca.uhn.fhir.rest.annotation.IdParam;
062import ca.uhn.fhir.rest.annotation.Initialize;
063import ca.uhn.fhir.rest.api.MethodOutcome;
064import ca.uhn.fhir.rest.api.RequestTypeEnum;
065import ca.uhn.fhir.rest.method.BaseMethodBinding;
066import ca.uhn.fhir.rest.method.ConformanceMethodBinding;
067import ca.uhn.fhir.rest.method.ParseAction;
068import ca.uhn.fhir.rest.method.RequestDetails;
069import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
070import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
071import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
072import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
073import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
074import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
075import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
076import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
077import ca.uhn.fhir.util.ReflectionUtil;
078import ca.uhn.fhir.util.UrlUtil;
079import ca.uhn.fhir.util.VersionUtil;
080
081public class RestfulServer extends HttpServlet implements IRestfulServer<ServletRequestDetails> {
082
083        /**
084         * Requests will have an HttpServletRequest attribute set with this name, containing the servlet
085         * context, in order to avoid a dependency on Servlet-API 3.0+ 
086         */
087        public static final String SERVLET_CONTEXT_ATTRIBUTE = "ca.uhn.fhir.rest.server.RestfulServer.servlet_context";
088        
089        /**
090         * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED}
091         */
092        public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED;
093        private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor();
094        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class);
095        private static final long serialVersionUID = 1L;
096        private AddProfileTagEnum myAddProfileTag;
097        private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES;
098        private boolean myDefaultPrettyPrint = false;
099        private EncodingEnum myDefaultResponseEncoding = EncodingEnum.XML;
100        private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT;
101        private FhirContext myFhirContext;
102        private String myImplementationDescription;
103        private final List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>();
104        private IPagingProvider myPagingProvider;
105        private final List<Object> myPlainProviders = new ArrayList<Object>();
106        private Lock myProviderRegistrationMutex = new ReentrantLock();
107        private Map<String, ResourceBinding> myResourceNameToBinding = new HashMap<String, ResourceBinding>();
108        private final List<IResourceProvider> myResourceProviders = new ArrayList<IResourceProvider>();
109        private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
110        private ResourceBinding myServerBinding = new ResourceBinding();
111        private BaseMethodBinding<?> myServerConformanceMethod;
112        private Object myServerConformanceProvider;
113        private String myServerName = "HAPI FHIR Server";
114        /** This is configurable but by default we just use HAPI version */
115        private String myServerVersion = VersionUtil.getVersion();
116        private boolean myStarted;
117        private Map<String, IResourceProvider> myTypeToProvider = new HashMap<String, IResourceProvider>();
118        private boolean myUncompressIncomingContents = true;
119        private boolean myUseBrowserFriendlyContentTypes;
120
121        /**
122         * Constructor. Note that if no {@link FhirContext} is passed in to the server (either through the constructor, or through {@link #setFhirContext(FhirContext)}) the server will determine which
123         * version of FHIR to support through classpath scanning. This is brittle, and it is highly recommended to explicitly specify a FHIR version.
124         */
125        public RestfulServer() {
126                this(null);
127        }
128
129        /**
130         * Constructor
131         */
132        public RestfulServer(FhirContext theCtx) {
133                myFhirContext = theCtx;
134        }
135
136        private void addContentLocationHeaders(RequestDetails theRequest, HttpServletResponse servletResponse, MethodOutcome response, String resourceName) {
137                if (response != null && response.getId() != null) {
138                        addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_LOCATION, resourceName);
139                        addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_CONTENT_LOCATION, resourceName);
140                }
141        }
142
143        /**
144         * This method is called prior to sending a response to incoming requests. It is used to add custom headers.
145         * <p>
146         * Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid inadvertantly disabling functionality.
147         * </p>
148         */
149        public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
150                theHttpResponse.addHeader("X-Powered-By", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server");
151        }
152
153        private void addLocationHeader(RequestDetails theRequest, HttpServletResponse theResponse, MethodOutcome response, String headerLocation, String resourceName) {
154                StringBuilder b = new StringBuilder();
155                b.append(theRequest.getFhirServerBase());
156                b.append('/');
157                b.append(resourceName);
158                b.append('/');
159                b.append(response.getId().getIdPart());
160                if (response.getId().hasVersionIdPart()) {
161                        b.append("/" + Constants.PARAM_HISTORY + "/");
162                        b.append(response.getId().getVersionIdPart());
163                } else if (response.getVersionId() != null && response.getVersionId().isEmpty() == false) {
164                        b.append("/" + Constants.PARAM_HISTORY + "/");
165                        b.append(response.getVersionId().getValue());
166                }
167                theResponse.addHeader(headerLocation, b.toString());
168
169        }
170
171        private void assertProviderIsValid(Object theNext) throws ConfigurationException {
172                if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) {
173                        throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class must be public");
174                }
175        }
176
177        public RestulfulServerConfiguration createConfiguration() {
178                RestulfulServerConfiguration result = new RestulfulServerConfiguration();
179                result.setResourceBindings(getResourceBindings());
180                result.setServerBindings(getServerBindings());
181                result.setImplementationDescription(getImplementationDescription());
182                result.setServerVersion(getServerVersion());
183                result.setServerName(getServerName());
184                result.setFhirContext(getFhirContext());
185                result.setServerAddressStrategy(myServerAddressStrategy);
186                InputStream inputStream = null;
187                try {
188                        inputStream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");
189                        if (inputStream != null) {
190                                Manifest manifest = new Manifest(inputStream);
191                                result.setConformanceDate(manifest.getMainAttributes().getValue("Build-Time"));
192                        }
193                } catch (IOException e) {
194                        // fall through
195                } finally {
196                        if (inputStream != null) {
197                                IOUtils.closeQuietly(inputStream);
198                        }
199                }
200                return result;
201        }
202
203        @Override
204        public void destroy() {
205                if (getResourceProviders() != null) {
206                        for (IResourceProvider iResourceProvider : getResourceProviders()) {
207                                invokeDestroy(iResourceProvider);
208                        }
209                }
210                if (myServerConformanceProvider != null) {
211                        invokeDestroy(myServerConformanceProvider);
212                }
213                if (getPlainProviders() != null) {
214                        for (Object next : getPlainProviders()) {
215                                invokeDestroy(next);
216                        }
217                }
218        }
219
220        public BaseMethodBinding<?> determineResourceMethod(RequestDetails requestDetails, String requestPath) {
221                RequestTypeEnum requestType = requestDetails.getRequestType();
222
223                ResourceBinding resourceBinding = null;
224                BaseMethodBinding<?> resourceMethod = null;
225                String resourceName = requestDetails.getResourceName();
226                if (Constants.URL_TOKEN_METADATA.equals(resourceName) || requestType == RequestTypeEnum.OPTIONS) {
227                        resourceMethod = myServerConformanceMethod;
228                } else if (resourceName == null) {
229                        resourceBinding = myServerBinding;
230                } else {
231                        resourceBinding = myResourceNameToBinding.get(resourceName);
232                        if (resourceBinding == null) {
233                                throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToBinding.keySet());
234                        }
235                }
236
237                if (resourceMethod == null) {
238                        if (resourceBinding != null) {
239                                resourceMethod = resourceBinding.getMethod(requestDetails);
240                        }
241                }
242                if (resourceMethod == null) {
243                        if (isBlank(requestPath)) {
244                                throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "rootRequest"));
245                        } else {
246                                throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "unknownMethod", requestType.name(), requestPath, requestDetails.getParameters().keySet()));
247                        }
248                }
249                return resourceMethod;
250        }
251
252        @Override
253        protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
254                handleRequest(RequestTypeEnum.DELETE, request, response);
255        }
256
257        @Override
258        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
259                handleRequest(RequestTypeEnum.GET, request, response);
260        }
261
262        @Override
263        protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException {
264                handleRequest(RequestTypeEnum.OPTIONS, theReq, theResp);
265        }
266
267        @Override
268        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
269                handleRequest(RequestTypeEnum.POST, request, response);
270        }
271
272        @Override
273        protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
274                handleRequest(RequestTypeEnum.PUT, request, response);
275        }
276
277        /**
278         * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
279         */
280        protected int escapedLength(String theServletPath) {
281                int delta = 0;
282                for (int i = 0; i < theServletPath.length(); i++) {
283                        char next = theServletPath.charAt(i);
284                        if (next == ' ') {
285                                delta = delta + 2;
286                        }
287                }
288                return theServletPath.length() + delta;
289        }
290
291        private void findResourceMethods(Object theProvider) throws Exception {
292
293                ourLog.info("Scanning type for RESTful methods: {}", theProvider.getClass());
294                int count = 0;
295
296                Class<?> clazz = theProvider.getClass();
297                Class<?> supertype = clazz.getSuperclass();
298                while (!Object.class.equals(supertype)) {
299                        count += findResourceMethods(theProvider, supertype);
300                        supertype = supertype.getSuperclass();
301                }
302
303                count += findResourceMethods(theProvider, clazz);
304
305                if (count == 0) {
306                        throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName());
307                }
308        }
309
310        private int findResourceMethods(Object theProvider, Class<?> clazz) throws ConfigurationException {
311                int count = 0;
312
313                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
314                        BaseMethodBinding<?> foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
315                        if (foundMethodBinding == null) {
316                                continue;
317                        }
318
319                        count++;
320
321                        if (foundMethodBinding instanceof ConformanceMethodBinding) {
322                                myServerConformanceMethod = foundMethodBinding;
323                                continue;
324                        }
325
326                        if (!Modifier.isPublic(m.getModifiers())) {
327                                throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public");
328                        } else {
329                                if (Modifier.isStatic(m.getModifiers())) {
330                                        throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static");
331                                } else {
332                                        ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName());
333
334                                        String resourceName = foundMethodBinding.getResourceName();
335                                        ResourceBinding resourceBinding;
336                                        if (resourceName == null) {
337                                                resourceBinding = myServerBinding;
338                                        } else {
339                                                RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName);
340                                                if (myResourceNameToBinding.containsKey(definition.getName())) {
341                                                        resourceBinding = myResourceNameToBinding.get(definition.getName());
342                                                } else {
343                                                        resourceBinding = new ResourceBinding();
344                                                        resourceBinding.setResourceName(resourceName);
345                                                        myResourceNameToBinding.put(resourceName, resourceBinding);
346                                                }
347                                        }
348
349                                        List<Class<?>> allowableParams = foundMethodBinding.getAllowableParamAnnotations();
350                                        if (allowableParams != null) {
351                                                for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) {
352                                                        for (Annotation annotation : nextParamAnnotations) {
353                                                                Package pack = annotation.annotationType().getPackage();
354                                                                if (pack.equals(IdParam.class.getPackage())) {
355                                                                        if (!allowableParams.contains(annotation.annotationType())) {
356                                                                                throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation);
357                                                                        }
358                                                                }
359                                                        }
360                                                }
361                                        }
362
363                                        resourceBinding.addMethod(foundMethodBinding);
364                                        ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName());
365                                }
366                        }
367                }
368
369                return count;
370        }
371
372        @Override
373        public AddProfileTagEnum getAddProfileTag() {
374                return myAddProfileTag;
375        }
376
377        @Override
378        public BundleInclusionRule getBundleInclusionRule() {
379                return myBundleInclusionRule;
380        }
381
382        /**
383         * Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the <code>_format</code> URL parameter, or with an <code>Accept</code> header
384         * in the request. The default is {@link EncodingEnum#XML}. Will not return null.
385         */
386        @Override
387        public EncodingEnum getDefaultResponseEncoding() {
388                return myDefaultResponseEncoding;
389        }
390
391        @Override
392        public ETagSupportEnum getETagSupport() {
393                return myETagSupport;
394        }
395
396        /**
397         * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to
398         * creating their own.
399         */
400        @Override
401        public FhirContext getFhirContext() {
402                if (myFhirContext == null) {
403                        myFhirContext = new FhirContext();
404                }
405                return myFhirContext;
406        }
407
408        public String getImplementationDescription() {
409                return myImplementationDescription;
410        }
411
412        /**
413         * Returns a ist of all registered server interceptors
414         */
415        @Override
416        public List<IServerInterceptor> getInterceptors() {
417                return Collections.unmodifiableList(myInterceptors);
418        }
419
420        @Override
421        public IPagingProvider getPagingProvider() {
422                return myPagingProvider;
423        }
424
425        /**
426         * Provides the non-resource specific providers which implement method calls on this server
427         *
428         * @see #getResourceProviders()
429         */
430        public Collection<Object> getPlainProviders() {
431                return myPlainProviders;
432        }
433
434        /**
435         * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path implementation
436         *
437         * @param requestFullPath
438         *           the full request path
439         * @param servletContextPath
440         *           the servelet context path
441         * @param servletPath
442         *           the servelet path
443         * @return created resource path
444         */
445        protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {
446                return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath));
447        }
448
449        public Collection<ResourceBinding> getResourceBindings() {
450                return myResourceNameToBinding.values();
451        }
452
453        /**
454         * Provides the resource providers for this server
455         */
456        public Collection<IResourceProvider> getResourceProviders() {
457                return myResourceProviders;
458        }
459
460        /**
461         * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
462         */
463        public IServerAddressStrategy getServerAddressStrategy() {
464                return myServerAddressStrategy;
465        }
466
467        /**
468         * Returns the server base URL (with no trailing '/') for a given request
469         */
470        public String getServerBaseForRequest(HttpServletRequest theRequest) {
471                String fhirServerBase;
472                fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest);
473
474                if (fhirServerBase.endsWith("/")) {
475                        fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1);
476                }
477                return fhirServerBase;
478        }
479
480        /**
481         * Returns the method bindings for this server which are not specific to any particular resource type. This method is internal to HAPI and developers generally do not need to interact with it. Use
482         * with caution, as it may change.
483         */
484        public List<BaseMethodBinding<?>> getServerBindings() {
485                return myServerBinding.getMethodBindings();
486        }
487
488        /**
489         * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement if one has been explicitly defined.
490         * <p>
491         * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> to use the appropriate one for the given FHIR version.
492         * </p>
493         */
494        public Object getServerConformanceProvider() {
495                return myServerConformanceProvider;
496        }
497
498        /**
499         * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
500         *
501         * @see RestfulServer#setServerName(String)
502         */
503        public String getServerName() {
504                return myServerName;
505        }
506
507        public IResourceProvider getServerProfilesProvider() {
508                return getFhirContext().getVersion().createServerProfilesProvider(this);
509        }
510
511        /**
512         * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
513         */
514        public String getServerVersion() {
515                return myServerVersion;
516        }
517
518        protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException {
519                String fhirServerBase = null;
520                boolean requestIsBrowser = requestIsBrowser(theRequest);
521                ServletRequestDetails requestDetails = new ServletRequestDetails();
522                requestDetails.setServer(this);
523                requestDetails.setRequestType(theRequestType);
524                requestDetails.setServletRequest(theRequest);
525                requestDetails.setServletResponse(theResponse);
526                
527                theRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, getServletContext());
528
529                try {
530
531                        for (IServerInterceptor next : myInterceptors) {
532                                boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse);
533                                if (!continueProcessing) {
534                                        ourLog.debug("Interceptor {} returned false, not continuing processing");
535                                        return;
536                                }
537                        }
538
539                        String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI());
540                        String servletPath = StringUtils.defaultString(theRequest.getServletPath());
541                        StringBuffer requestUrl = theRequest.getRequestURL();
542                        String servletContextPath = IncomingRequestAddressStrategy.determineServletContextPath(theRequest, this);
543
544                        /*
545                         * Just for debugging..
546                         */
547                        if (ourLog.isTraceEnabled()) {
548                                ourLog.trace("Request FullPath: {}", requestFullPath);
549                                ourLog.trace("Servlet Path: {}", servletPath);
550                                ourLog.trace("Request Url: {}", requestUrl);
551                                ourLog.trace("Context Path: {}", servletContextPath);
552                        }
553
554                        String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath);
555
556                        if (requestPath.length() > 0 && requestPath.charAt(0) == '/') {
557                                requestPath = requestPath.substring(1);
558                        }
559
560                        fhirServerBase = getServerBaseForRequest(theRequest);
561
562                        String completeUrl = StringUtils.isNotBlank(theRequest.getQueryString()) ? requestUrl + "?" + theRequest.getQueryString() : requestUrl.toString();
563
564                        Map<String, String[]> params = new HashMap<String, String[]>(theRequest.getParameterMap());
565                        requestDetails.setParameters(params);
566
567                        IIdType id;
568                        populateRequestDetailsFromRequestPath(requestDetails, requestPath);
569
570                        if (theRequestType == RequestTypeEnum.PUT) {
571                                String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION);
572                                if (contentLocation != null) {
573                                        id = myFhirContext.getVersion().newIdType();
574                                        id.setValue(contentLocation);
575                                        requestDetails.setId(id);
576                                }
577                        }
578
579                        String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING);
580                        boolean respondGzip = false;
581                        if (acceptEncoding != null) {
582                                String[] parts = acceptEncoding.trim().split("\\s*,\\s*");
583                                for (String string : parts) {
584                                        if (string.equals("gzip")) {
585                                                respondGzip = true;
586                                        }
587                                }
588                        }
589                        requestDetails.setRespondGzip(respondGzip);
590                        requestDetails.setRequestPath(requestPath);
591                        requestDetails.setFhirServerBase(fhirServerBase);
592                        requestDetails.setCompleteUrl(completeUrl);
593
594                        // String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION);
595                        // if (getPagingProvider() != null && isNotBlank(pagingAction)) {
596                        // requestDetails.setRestOperationType(RestOperationTypeEnum.GET_PAGE);
597                        // if (theRequestType != RequestTypeEnum.GET) {
598                        // /*
599                        // * We reconstruct the link-self URL using the request parameters, and this would break if the parameters came
600                        // in using a POST. We could probably work around that but why bother unless
601                        // * someone comes up with a reason for needing it.
602                        // */
603                        // throw new InvalidRequestException(getFhirContext().getLocalizer().getMessage(RestfulServer.class,
604                        // "getPagesNonHttpGet"));
605                        // }
606                        // handlePagingRequest(requestDetails, theResponse, pagingAction);
607                        // return;
608                        // }
609
610                        BaseMethodBinding<?> resourceMethod = determineResourceMethod(requestDetails, requestPath);
611
612                        requestDetails.setRestOperationType(resourceMethod.getRestOperationType());
613
614                        // Handle server interceptors
615                        for (IServerInterceptor next : myInterceptors) {
616                                boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse);
617                                if (!continueProcessing) {
618                                        ourLog.debug("Interceptor {} returned false, not continuing processing");
619                                        return;
620                                }
621                        }
622
623                        /*
624                         * Actualy invoke the server method. This call is to a HAPI method binding, which is an object that wraps a specific implementing (user-supplied) method, but handles its input and provides
625                         * its output back to the client.
626                         * 
627                         * This is basically the end of processing for a successful request, since the method binding replies to the client and closes the response.
628                         */
629                        resourceMethod.invokeServer(this, requestDetails);
630
631                } catch (NotModifiedException e) {
632
633                        for (int i = getInterceptors().size() - 1; i >= 0; i--) {
634                                IServerInterceptor next = getInterceptors().get(i);
635                                if (!next.handleException(requestDetails, e, theRequest, theResponse)) {
636                                        ourLog.debug("Interceptor {} returned false, not continuing processing");
637                                        return;
638                                }
639                        }
640                        writeExceptionToResponse(theResponse, e);
641
642                } catch (AuthenticationException e) {
643
644                        for (int i = getInterceptors().size() - 1; i >= 0; i--) {
645                                IServerInterceptor next = getInterceptors().get(i);
646                                if (!next.handleException(requestDetails, e, theRequest, theResponse)) {
647                                        ourLog.debug("Interceptor {} returned false, not continuing processing");
648                                        return;
649                                }
650                        }
651
652                        if (requestIsBrowser) {
653                                // if request is coming from a browser, prompt the user to enter login credentials
654                                theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\"");
655                        }
656                        writeExceptionToResponse(theResponse, e);
657
658                } catch (Throwable e) {
659
660                        /*
661                         * We have caught an exception during request processing. This might be because a handling method threw something they wanted to throw (e.g. UnprocessableEntityException because the request
662                         * had business requirement problems) or it could be due to bugs (e.g. NullPointerException).
663                         * 
664                         * First we let the interceptors have a crack at converting the exception into something HAPI can use (BaseServerResponseException)
665                         */
666                        BaseServerResponseException exception = null;
667                        for (int i = getInterceptors().size() - 1; i >= 0; i--) {
668                                IServerInterceptor next = getInterceptors().get(i);
669                                exception = next.preProcessOutgoingException(requestDetails, e, theRequest);
670                                if (exception != null) {
671                                        ourLog.debug("Interceptor {} returned false, not continuing processing");
672                                        break;
673                                }
674                        }
675
676                        /*
677                         * If none of the interceptors converted the exception, default behaviour is to keep the exception as-is if it extends BaseServerResponseException, otherwise wrap it in an
678                         * InternalErrorException.
679                         */
680                        if (exception == null) {
681                                exception = DEFAULT_EXCEPTION_HANDLER.preProcessOutgoingException(requestDetails, e, theRequest);
682                        }
683
684                        /*
685                         * Next, interceptors get a shot at handling the exception
686                         */
687                        for (int i = getInterceptors().size() - 1; i >= 0; i--) {
688                                IServerInterceptor next = getInterceptors().get(i);
689                                if (!next.handleException(requestDetails, exception, theRequest, theResponse)) {
690                                        ourLog.debug("Interceptor {} returned false, not continuing processing");
691                                        return;
692                                }
693                        }
694
695                        /*
696                         * If we're handling an exception, no summary mode should be applied
697                         */
698                        requestDetails.getParameters().remove(Constants.PARAM_SUMMARY);
699                        requestDetails.getParameters().remove(Constants.PARAM_ELEMENTS);
700
701                        /*
702                         * If nobody handles it, default behaviour is to stream back the OperationOutcome to the client.
703                         */
704                        DEFAULT_EXCEPTION_HANDLER.handleException(requestDetails, exception, theRequest, theResponse);
705
706                }
707        }
708
709        /**
710         * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, but subclasses may put initialization code in {@link #initialize()}, which is
711         * called immediately before beginning initialization of the restful server's internal init.
712         */
713        @Override
714        public final void init() throws ServletException {
715                myProviderRegistrationMutex.lock();
716                try {
717                        initialize();
718
719                        Object confProvider;
720                        try {
721                                ourLog.info("Initializing HAPI FHIR restful server running in " + getFhirContext().getVersion().getVersion().name() + " mode");
722
723                                ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext());
724                                providedResourceScanner.scanForProvidedResources(this);
725
726                                Collection<IResourceProvider> resourceProvider = getResourceProviders();
727                                // 'true' tells registerProviders() that
728                                // this call is part of initialization
729                                registerProviders(resourceProvider, true);
730
731                                Collection<Object> providers = getPlainProviders();
732                                // 'true' tells registerProviders() that
733                                // this call is part of initialization
734                                registerProviders(providers, true);
735
736                                findResourceMethods(getServerProfilesProvider());
737
738                                confProvider = getServerConformanceProvider();
739                                if (confProvider == null) {
740                                        confProvider = getFhirContext().getVersion().createServerConformanceProvider(this);
741                                }
742                                // findSystemMethods(confProvider);
743                                findResourceMethods(confProvider);
744
745                        } catch (Exception ex) {
746                                ourLog.error("An error occurred while loading request handlers!", ex);
747                                throw new ServletException("Failed to initialize FHIR Restful server", ex);
748                        }
749
750                        ourLog.trace("Invoking provider initialize methods");
751                        if (getResourceProviders() != null) {
752                                for (IResourceProvider iResourceProvider : getResourceProviders()) {
753                                        invokeInitialize(iResourceProvider);
754                                }
755                        }
756                        if (confProvider != null) {
757                                invokeInitialize(confProvider);
758                        }
759                        if (getPlainProviders() != null) {
760                                for (Object next : getPlainProviders()) {
761                                        invokeInitialize(next);
762                                }
763                        }
764
765                        try {
766                                findResourceMethods(new PageProvider());
767                        } catch (Exception ex) {
768                                ourLog.error("An error occurred while loading request handlers!", ex);
769                                throw new ServletException("Failed to initialize FHIR Restful server", ex);
770                        }
771
772                        myStarted = true;
773                        ourLog.info("A FHIR has been lit on this server");
774                } finally {
775                        myProviderRegistrationMutex.unlock();
776                }
777        }
778
779        /**
780         * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used.
781         * 
782         * @throws ServletException
783         *            If the initialization failed. Note that you should consider throwing {@link UnavailableException} (which extends {@link ServletException}), as this is a flag to the servlet container
784         *            that the servlet is not usable.
785         */
786        protected void initialize() throws ServletException {
787                // nothing by default
788        }
789
790        private void invokeDestroy(Object theProvider) {
791                invokeDestroy(theProvider, theProvider.getClass());
792        }
793
794        private void invokeDestroy(Object theProvider, Class<?> clazz) {
795                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
796                        Destroy destroy = m.getAnnotation(Destroy.class);
797                        if (destroy != null) {
798                                try {
799                                        m.invoke(theProvider);
800                                } catch (IllegalAccessException e) {
801                                        ourLog.error("Exception occurred in destroy ", e);
802                                } catch (InvocationTargetException e) {
803                                        ourLog.error("Exception occurred in destroy ", e);
804                                }
805                                return;
806                        }
807                }
808
809                Class<?> supertype = clazz.getSuperclass();
810                if (!Object.class.equals(supertype)) {
811                        invokeDestroy(theProvider, supertype);
812                }
813        }
814
815        private void invokeInitialize(Object theProvider) {
816                invokeInitialize(theProvider, theProvider.getClass());
817        }
818
819        private void invokeInitialize(Object theProvider, Class<?> clazz) {
820                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
821                        Initialize initialize = m.getAnnotation(Initialize.class);
822                        if (initialize != null) {
823                                try {
824                                        m.invoke(theProvider);
825                                } catch (IllegalAccessException e) {
826                                        ourLog.error("Exception occurred in destroy ", e);
827                                } catch (InvocationTargetException e) {
828                                        ourLog.error("Exception occurred in destroy ", e);
829                                }
830                                return;
831                        }
832                }
833
834                Class<?> supertype = clazz.getSuperclass();
835                if (!Object.class.equals(supertype)) {
836                        invokeInitialize(theProvider, supertype);
837                }
838        }
839
840        /**
841         * Should the server "pretty print" responses by default (requesting clients can always override this default by supplying an <code>Accept</code> header in the request, or a <code>_pretty</code>
842         * parameter in the request URL.
843         * <p>
844         * The default is <code>false</code>
845         * </p>
846         * 
847         * @return Returns the default pretty print setting
848         */
849        @Override
850        public boolean isDefaultPrettyPrint() {
851                return myDefaultPrettyPrint;
852        }
853
854        /**
855         * Should the server attempt to decompress incoming request contents (default is <code>true</code>). Typically this should be set to <code>true</code> unless the server has other configuration to
856         * deal with decompressing request bodies (e.g. a filter applied to the whole server).
857         */
858        public boolean isUncompressIncomingContents() {
859                return myUncompressIncomingContents;
860        }
861
862        /**
863         * @deprecated This feature did not work well, and will be removed. Use {@link ResponseHighlighterInterceptor} instead as an interceptor on your server and it will provide more useful syntax
864         *             highlighting. Deprocated in 1.4
865         */
866        @Deprecated
867        @Override
868        public boolean isUseBrowserFriendlyContentTypes() {
869                return myUseBrowserFriendlyContentTypes;
870        }
871
872        public void populateRequestDetailsFromRequestPath(RequestDetails theRequestDetails, String theRequestPath) {
873                StringTokenizer tok = new StringTokenizer(theRequestPath, "/");
874                String resourceName = null;
875
876                IIdType id = null;
877                String operation = null;
878                String compartment = null;
879                if (tok.hasMoreTokens()) {
880                        resourceName = tok.nextToken();
881                        if (partIsOperation(resourceName)) {
882                                operation = resourceName;
883                                resourceName = null;
884                        }
885                }
886                theRequestDetails.setResourceName(resourceName);
887
888                if (tok.hasMoreTokens()) {
889                        String nextString = tok.nextToken();
890                        if (partIsOperation(nextString)) {
891                                operation = nextString;
892                        } else {
893                                id = myFhirContext.getVersion().newIdType();
894                                id.setParts(null, resourceName, UrlUtil.unescape(nextString), null);
895                        }
896                }
897
898                if (tok.hasMoreTokens()) {
899                        String nextString = tok.nextToken();
900                        if (nextString.equals(Constants.PARAM_HISTORY)) {
901                                if (tok.hasMoreTokens()) {
902                                        String versionString = tok.nextToken();
903                                        if (id == null) {
904                                                throw new InvalidRequestException("Don't know how to handle request path: " + theRequestPath);
905                                        }
906                                        id.setParts(null, resourceName, id.getIdPart(), UrlUtil.unescape(versionString));
907                                } else {
908                                        operation = Constants.PARAM_HISTORY;
909                                }
910                        } else if (partIsOperation(nextString)) {
911                                if (operation != null) {
912                                        throw new InvalidRequestException("URL Path contains two operations: " + theRequestPath);
913                                }
914                                operation = nextString;
915                        } else {
916                                compartment = nextString;
917                        }
918                }
919
920                // Secondary is for things like ..../_tags/_delete
921                String secondaryOperation = null;
922
923                while (tok.hasMoreTokens()) {
924                        String nextString = tok.nextToken();
925                        if (operation == null) {
926                                operation = nextString;
927                        } else if (secondaryOperation == null) {
928                                secondaryOperation = nextString;
929                        } else {
930                                throw new InvalidRequestException("URL path has unexpected token '" + nextString + "' at the end: " + theRequestPath);
931                        }
932                }
933
934                theRequestDetails.setId(id);
935                theRequestDetails.setOperation(operation);
936                theRequestDetails.setSecondaryOperation(secondaryOperation);
937                theRequestDetails.setCompartmentName(compartment);
938        }
939
940        public void registerInterceptor(IServerInterceptor theInterceptor) {
941                Validate.notNull(theInterceptor, "Interceptor can not be null");
942                myInterceptors.add(theInterceptor);
943        }
944
945        /**
946         * Register a single provider. This could be a Resource Provider or a "plain" provider not associated with any resource.
947         * 
948         * @param provider
949         * @throws Exception
950         */
951        public void registerProvider(Object provider) throws Exception {
952                if (provider != null) {
953                        Collection<Object> providerList = new ArrayList<Object>(1);
954                        providerList.add(provider);
955                        registerProviders(providerList);
956                }
957        }
958
959        /**
960         * Register a group of providers. These could be Resource Providers, "plain" providers or a mixture of the two.
961         * 
962         * @param providers
963         *           a {@code Collection} of providers. The parameter could be null or an empty {@code Collection}
964         * @throws Exception
965         */
966        public void registerProviders(Collection<? extends Object> providers) throws Exception {
967                myProviderRegistrationMutex.lock();
968                try {
969                        if (!myStarted) {
970                                for (Object provider : providers) {
971                                        ourLog.info("Registration of provider [" + provider.getClass().getName() + "] will be delayed until FHIR server startup");
972                                        if (provider instanceof IResourceProvider) {
973                                                myResourceProviders.add((IResourceProvider) provider);
974                                        } else {
975                                                myPlainProviders.add(provider);
976                                        }
977                                }
978                                return;
979                        }
980                } finally {
981                        myProviderRegistrationMutex.unlock();
982                }
983                registerProviders(providers, false);
984        }
985
986        /*
987         * Inner method to actually register providers
988         */
989        protected void registerProviders(Collection<? extends Object> providers, boolean inInit) throws Exception {
990                List<IResourceProvider> newResourceProviders = new ArrayList<IResourceProvider>();
991                List<Object> newPlainProviders = new ArrayList<Object>();
992                ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext());
993
994                if (providers != null) {
995                        for (Object provider : providers) {
996                                if (provider instanceof IResourceProvider) {
997                                        IResourceProvider rsrcProvider = (IResourceProvider) provider;
998                                        Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType();
999                                        if (resourceType == null) {
1000                                                throw new NullPointerException("getResourceType() on class '" + rsrcProvider.getClass().getCanonicalName() + "' returned null");
1001                                        }
1002                                        String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
1003                                        if (myTypeToProvider.containsKey(resourceName)) {
1004                                                throw new ServletException("Multiple resource providers return resource type[" + resourceName + "]: First[" + myTypeToProvider.get(resourceName).getClass().getCanonicalName()
1005                                                                + "] and Second[" + rsrcProvider.getClass().getCanonicalName() + "]");
1006                                        }
1007                                        if (!inInit) {
1008                                                myResourceProviders.add(rsrcProvider);
1009                                        }
1010                                        myTypeToProvider.put(resourceName, rsrcProvider);
1011                                        providedResourceScanner.scanForProvidedResources(rsrcProvider);
1012                                        newResourceProviders.add(rsrcProvider);
1013                                } else {
1014                                        if (!inInit) {
1015                                                myPlainProviders.add(provider);
1016                                        }
1017                                        newPlainProviders.add(provider);
1018                                }
1019
1020                        }
1021                        if (!newResourceProviders.isEmpty()) {
1022                                ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myTypeToProvider.size());
1023                                for (IResourceProvider provider : newResourceProviders) {
1024                                        assertProviderIsValid(provider);
1025                                        findResourceMethods(provider);
1026                                }
1027                        }
1028                        if (!newPlainProviders.isEmpty()) {
1029                                ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size());
1030                                for (Object provider : newPlainProviders) {
1031                                        assertProviderIsValid(provider);
1032                                        findResourceMethods(provider);
1033                                }
1034                        }
1035                        if (!inInit) {
1036                                ourLog.trace("Invoking provider initialize methods");
1037                                if (!newResourceProviders.isEmpty()) {
1038                                        for (IResourceProvider provider : newResourceProviders) {
1039                                                invokeInitialize(provider);
1040                                        }
1041                                }
1042                                if (!newPlainProviders.isEmpty()) {
1043                                        for (Object provider : newPlainProviders) {
1044                                                invokeInitialize(provider);
1045                                        }
1046                                }
1047                        }
1048                }
1049        }
1050
1051        /*
1052         * Remove registered RESTful methods for a Provider (and all superclasses) when it is being unregistered
1053         */
1054        private void removeResourceMethods(Object theProvider) throws Exception {
1055                ourLog.info("Removing RESTful methods for: {}", theProvider.getClass());
1056                Class<?> clazz = theProvider.getClass();
1057                Class<?> supertype = clazz.getSuperclass();
1058                Collection<String> resourceNames = new ArrayList<String>();
1059                while (!Object.class.equals(supertype)) {
1060                        removeResourceMethods(theProvider, supertype, resourceNames);
1061                        supertype = supertype.getSuperclass();
1062                }
1063                removeResourceMethods(theProvider, clazz, resourceNames);
1064                for (String resourceName : resourceNames) {
1065                        myResourceNameToBinding.remove(resourceName);
1066                }
1067        }
1068
1069        /*
1070         * Collect the set of RESTful methods for a single class when it is being unregistered
1071         */
1072        private void removeResourceMethods(Object theProvider, Class<?> clazz, Collection<String> resourceNames) throws ConfigurationException {
1073                for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
1074                        BaseMethodBinding<?> foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
1075                        if (foundMethodBinding == null) {
1076                                continue; // not a bound method
1077                        }
1078                        if (foundMethodBinding instanceof ConformanceMethodBinding) {
1079                                myServerConformanceMethod = null;
1080                                continue;
1081                        }
1082                        String resourceName = foundMethodBinding.getResourceName();
1083                        if (!resourceNames.contains(resourceName)) {
1084                                resourceNames.add(resourceName);
1085                        }
1086                }
1087        }
1088
1089        public Object returnResponse(ServletRequestDetails theRequest, ParseAction<?> outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, String resourceName) throws IOException {
1090                HttpServletResponse servletResponse = theRequest.getServletResponse();
1091                servletResponse.setStatus(operationStatus);
1092                servletResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8);
1093                addHeadersToResponse(servletResponse);
1094                if (allowPrefer) {
1095                        addContentLocationHeaders(theRequest, servletResponse, response, resourceName);
1096                }
1097                if (outcome != null) {
1098                        EncodingEnum encoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequest);
1099                        servletResponse.setContentType(encoding.getResourceContentType());
1100                        Writer writer = servletResponse.getWriter();
1101                        IParser parser = encoding.newParser(getFhirContext());
1102                        parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(this, theRequest));
1103                        try {
1104                                outcome.execute(parser, writer);
1105                        } finally {
1106                                writer.close();
1107                        }
1108                } else {
1109                        servletResponse.setContentType(Constants.CT_TEXT_WITH_UTF8);
1110                        Writer writer = servletResponse.getWriter();
1111                        writer.close();
1112                }
1113                // getMethod().in
1114                return null;
1115        }
1116
1117        /**
1118         * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} (which is the default), the server will automatically add a profile tag based on
1119         * the class of the resource(s) being returned.
1120         *
1121         * @param theAddProfileTag
1122         *           The behaviour enum (must not be null)
1123         */
1124        public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) {
1125                Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null");
1126                myAddProfileTag = theAddProfileTag;
1127        }
1128
1129        /**
1130         * Set how bundle factory should decide whether referenced resources should be included in bundles
1131         *
1132         * @param theBundleInclusionRule
1133         *           - inclusion rule (@see BundleInclusionRule for behaviors)
1134         */
1135        public void setBundleInclusionRule(BundleInclusionRule theBundleInclusionRule) {
1136                myBundleInclusionRule = theBundleInclusionRule;
1137        }
1138
1139        /**
1140         * Should the server "pretty print" responses by default (requesting clients can always override this default by supplying an <code>Accept</code> header in the request, or a <code>_pretty</code>
1141         * parameter in the request URL.
1142         * <p>
1143         * The default is <code>false</code>
1144         * </p>
1145         * 
1146         * @param theDefaultPrettyPrint
1147         *           The default pretty print setting
1148         */
1149        public void setDefaultPrettyPrint(boolean theDefaultPrettyPrint) {
1150                myDefaultPrettyPrint = theDefaultPrettyPrint;
1151        }
1152
1153        /**
1154         * Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the <code>_format</code> URL parameter, or with an <code>Accept</code> header in
1155         * the request. The default is {@link EncodingEnum#XML}.
1156         * <p>
1157         * Note when testing this feature: Some browsers will include "application/xml" in their Accept header, which means that the
1158         * </p>
1159         */
1160        public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) {
1161                Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null");
1162                myDefaultResponseEncoding = theDefaultResponseEncoding;
1163        }
1164
1165        /**
1166         * Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is {@link #DEFAULT_ETAG_SUPPORT}
1167         * 
1168         * @param theETagSupport
1169         *           The ETag support mode
1170         */
1171        public void setETagSupport(ETagSupportEnum theETagSupport) {
1172                if (theETagSupport == null) {
1173                        throw new NullPointerException("theETagSupport can not be null");
1174                }
1175                myETagSupport = theETagSupport;
1176        }
1177
1178        public void setFhirContext(FhirContext theFhirContext) {
1179                Validate.notNull(theFhirContext, "FhirContext must not be null");
1180                myFhirContext = theFhirContext;
1181        }
1182
1183        public void setImplementationDescription(String theImplementationDescription) {
1184                myImplementationDescription = theImplementationDescription;
1185        }
1186
1187        /**
1188         * Sets (or clears) the list of interceptors
1189         *
1190         * @param theList
1191         *           The list of interceptors (may be null)
1192         */
1193        public void setInterceptors(IServerInterceptor... theList) {
1194                myInterceptors.clear();
1195                if (theList != null) {
1196                        myInterceptors.addAll(Arrays.asList(theList));
1197                }
1198        }
1199
1200        /**
1201         * Sets (or clears) the list of interceptors
1202         *
1203         * @param theList
1204         *           The list of interceptors (may be null)
1205         */
1206        public void setInterceptors(List<IServerInterceptor> theList) {
1207                myInterceptors.clear();
1208                if (theList != null) {
1209                        myInterceptors.addAll(theList);
1210                }
1211        }
1212
1213        /**
1214         * Sets the paging provider to use, or <code>null</code> to use no paging (which is the default)
1215         */
1216        public void setPagingProvider(IPagingProvider thePagingProvider) {
1217                myPagingProvider = thePagingProvider;
1218        }
1219
1220        /**
1221         * Sets the non-resource specific providers which implement method calls on this server.
1222         *
1223         * @see #setResourceProviders(Collection)
1224         */
1225        public void setPlainProviders(Collection<Object> theProviders) {
1226                myPlainProviders.clear();
1227                if (theProviders != null) {
1228                        myPlainProviders.addAll(theProviders);
1229                }
1230        }
1231
1232        /**
1233         * Sets the non-resource specific providers which implement method calls on this server.
1234         *
1235         * @see #setResourceProviders(Collection)
1236         */
1237        public void setPlainProviders(Object... theProv) {
1238                setPlainProviders(Arrays.asList(theProv));
1239        }
1240
1241        /**
1242         * Sets the non-resource specific providers which implement method calls on this server
1243         *
1244         * @see #setResourceProviders(Collection)
1245         */
1246        public void setProviders(Object... theProviders) {
1247                myPlainProviders.clear();
1248                if (theProviders != null) {
1249                        myPlainProviders.addAll(Arrays.asList(theProviders));
1250                }
1251        }
1252
1253        /**
1254         * Sets the resource providers for this server
1255         */
1256        public void setResourceProviders(Collection<IResourceProvider> theResourceProviders) {
1257                myResourceProviders.clear();
1258                if (theResourceProviders != null) {
1259                        myResourceProviders.addAll(theResourceProviders);
1260                }
1261        }
1262
1263        /**
1264         * Sets the resource providers for this server
1265         */
1266        public void setResourceProviders(IResourceProvider... theResourceProviders) {
1267                myResourceProviders.clear();
1268                if (theResourceProviders != null) {
1269                        myResourceProviders.addAll(Arrays.asList(theResourceProviders));
1270                }
1271        }
1272
1273        /**
1274         * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
1275         */
1276        public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) {
1277                Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null");
1278                myServerAddressStrategy = theServerAddressStrategy;
1279        }
1280
1281        /**
1282         * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
1283         * <p>
1284         * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> if you do not wish to export a conformance
1285         * statement.
1286         * </p>
1287         * Note that this method can only be called before the server is initialized.
1288         *
1289         * @throws IllegalStateException
1290         *            Note that this method can only be called prior to {@link #init() initialization} and will throw an {@link IllegalStateException} if called after that.
1291         */
1292        public void setServerConformanceProvider(Object theServerConformanceProvider) {
1293                if (myStarted) {
1294                        throw new IllegalStateException("Server is already started");
1295                }
1296
1297                // call the setRestfulServer() method to point the Conformance
1298                // Provider to this server instance. This is done to avoid
1299                // passing the server into the constructor. Having that sort
1300                // of cross linkage causes reference cycles in Spring wiring
1301                try {
1302                        Method setRestfulServer = theServerConformanceProvider.getClass().getMethod("setRestfulServer", new Class[] { RestfulServer.class });
1303                        if (setRestfulServer != null) {
1304                                setRestfulServer.invoke(theServerConformanceProvider, new Object[] { this });
1305                        }
1306                } catch (Exception e) {
1307                        ourLog.warn("Error calling IServerConformanceProvider.setRestfulServer", e);
1308                }
1309                myServerConformanceProvider = theServerConformanceProvider;
1310        }
1311
1312        /**
1313         * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
1314         */
1315        public void setServerName(String theServerName) {
1316                myServerName = theServerName;
1317        }
1318
1319        /**
1320         * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
1321         */
1322        public void setServerVersion(String theServerVersion) {
1323                myServerVersion = theServerVersion;
1324        }
1325
1326        /**
1327         * Should the server attempt to decompress incoming request contents (default is <code>true</code>). Typically this should be set to <code>true</code> unless the server has other configuration to
1328         * deal with decompressing request bodies (e.g. a filter applied to the whole server).
1329         */
1330        public void setUncompressIncomingContents(boolean theUncompressIncomingContents) {
1331                myUncompressIncomingContents = theUncompressIncomingContents;
1332        }
1333
1334        /**
1335         * @deprecated This feature did not work well, and will be removed. Use {@link ResponseHighlighterInterceptor} instead as an interceptor on your server and it will provide more useful syntax
1336         *             highlighting. Deprocated in 1.4
1337         */
1338        @Deprecated
1339        public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) {
1340                myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes;
1341        }
1342
1343        public void unregisterInterceptor(IServerInterceptor theInterceptor) {
1344                Validate.notNull(theInterceptor, "Interceptor can not be null");
1345                myInterceptors.remove(theInterceptor);
1346        }
1347
1348        /**
1349         * Unregister one provider (either a Resource provider or a plain provider)
1350         * 
1351         * @param provider
1352         * @throws Exception
1353         */
1354        public void unregisterProvider(Object provider) throws Exception {
1355                if (provider != null) {
1356                        Collection<Object> providerList = new ArrayList<Object>(1);
1357                        providerList.add(provider);
1358                        unregisterProviders(providerList);
1359                }
1360        }
1361
1362        /**
1363         * Unregister a {@code Collection} of providers
1364         * 
1365         * @param providers
1366         * @throws Exception
1367         */
1368        public void unregisterProviders(Collection<? extends Object> providers) throws Exception {
1369                ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext());
1370                if (providers != null) {
1371                        for (Object provider : providers) {
1372                                removeResourceMethods(provider);
1373                                if (provider instanceof IResourceProvider) {
1374                                        myResourceProviders.remove(provider);
1375                                        IResourceProvider rsrcProvider = (IResourceProvider) provider;
1376                                        Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType();
1377                                        String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
1378                                        myTypeToProvider.remove(resourceName);
1379                                        providedResourceScanner.removeProvidedResources(rsrcProvider);
1380                                } else {
1381                                        myPlainProviders.remove(provider);
1382                                }
1383                                invokeDestroy(provider);
1384                        }
1385                }
1386        }
1387
1388        private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException) throws IOException {
1389                theResponse.setStatus(theException.getStatusCode());
1390                addHeadersToResponse(theResponse);
1391                theResponse.setContentType("text/plain");
1392                theResponse.setCharacterEncoding("UTF-8");
1393                theResponse.getWriter().write(theException.getMessage());
1394        }
1395
1396        private static boolean partIsOperation(String nextString) {
1397                return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$');
1398        }
1399
1400        public static boolean requestIsBrowser(HttpServletRequest theRequest) {
1401                String userAgent = theRequest.getHeader("User-Agent");
1402                return userAgent != null && userAgent.contains("Mozilla");
1403        }
1404
1405}