001package ca.uhn.fhir.jaxrs.server;
002
003/*
004 * #%L
005 * HAPI FHIR JAX-RS Server
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 java.io.IOException;
024import java.lang.annotation.Annotation;
025import java.lang.reflect.Method;
026import java.lang.reflect.Modifier;
027import java.util.*;
028import java.util.Map.Entry;
029import java.util.concurrent.ConcurrentHashMap;
030
031import javax.annotation.PostConstruct;
032import javax.ws.rs.*;
033import javax.ws.rs.core.MediaType;
034import javax.ws.rs.core.Response;
035
036import ca.uhn.fhir.interceptor.api.IInterceptorService;
037import org.apache.commons.lang3.StringUtils;
038import org.hl7.fhir.instance.model.api.IBaseResource;
039import org.slf4j.LoggerFactory;
040
041import ca.uhn.fhir.context.*;
042import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder;
043import ca.uhn.fhir.rest.annotation.IdParam;
044import ca.uhn.fhir.rest.api.*;
045import ca.uhn.fhir.rest.api.server.IRestfulResponse;
046import ca.uhn.fhir.rest.server.*;
047import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
048import ca.uhn.fhir.util.ReflectionUtil;
049
050/**
051 * This is the conformance provider for the jax rs servers. It requires all providers to be registered during startup because the conformance profile is generated during the postconstruct phase.
052 * 
053 * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
054 */
055@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
056public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProvider implements IResourceProvider {
057
058        /** the logger */
059        private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(AbstractJaxRsConformanceProvider.class);
060        /** the server bindings */
061        private ResourceBinding myServerBinding = new ResourceBinding();
062        /** the resource bindings */
063        private ConcurrentHashMap<String, ResourceBinding> myResourceNameToBinding = new ConcurrentHashMap<String, ResourceBinding>();
064        /** the server configuration */
065        private RestulfulServerConfiguration serverConfiguration = new RestulfulServerConfiguration();
066
067        /** the conformance. It is created once during startup */
068        private org.hl7.fhir.r4.model.CapabilityStatement myR4CapabilityStatement;
069        private org.hl7.fhir.dstu3.model.CapabilityStatement myDstu3CapabilityStatement;
070        private org.hl7.fhir.dstu2016may.model.Conformance myDstu2_1Conformance;
071        private org.hl7.fhir.instance.model.Conformance myDstu2Hl7OrgConformance;
072        private ca.uhn.fhir.model.dstu2.resource.Conformance myDstu2Conformance;
073        private boolean myInitialized;
074
075        /**
076         * Constructor allowing the description, servername and server to be set
077         * 
078         * @param implementationDescription
079         *           the implementation description. If null, "" is used
080         * @param serverName
081         *           the server name. If null, "" is used
082         * @param serverVersion
083         *           the server version. If null, "" is used
084         */
085        protected AbstractJaxRsConformanceProvider(String implementationDescription, String serverName, String serverVersion) {
086                serverConfiguration.setFhirContext(getFhirContext());
087                serverConfiguration.setImplementationDescription(StringUtils.defaultIfEmpty(implementationDescription, ""));
088                serverConfiguration.setServerName(StringUtils.defaultIfEmpty(serverName, ""));
089                serverConfiguration.setServerVersion(StringUtils.defaultIfEmpty(serverVersion, ""));
090        }
091
092        /**
093         * Constructor allowing the description, servername and server to be set
094         * 
095         * @param ctx
096         *           the {@link FhirContext} instance.
097         * @param implementationDescription
098         *           the implementation description. If null, "" is used
099         * @param serverName
100         *           the server name. If null, "" is used
101         * @param serverVersion
102         *           the server version. If null, "" is used
103         */
104        protected AbstractJaxRsConformanceProvider(FhirContext ctx, String implementationDescription, String serverName, String serverVersion) {
105                super(ctx);
106                serverConfiguration.setFhirContext(ctx);
107                serverConfiguration.setImplementationDescription(StringUtils.defaultIfEmpty(implementationDescription, ""));
108                serverConfiguration.setServerName(StringUtils.defaultIfEmpty(serverName, ""));
109                serverConfiguration.setServerVersion(StringUtils.defaultIfEmpty(serverVersion, ""));
110        }
111
112        /**
113         * This method will set the conformance during the postconstruct phase. The method {@link AbstractJaxRsConformanceProvider#getProviders()} is used to get all the resource providers include in the
114         * conformance
115         */
116        @PostConstruct
117        protected synchronized void setUpPostConstruct() {
118                if (myInitialized) {
119                        return;
120                }
121
122                for (Entry<Class<? extends IResourceProvider>, IResourceProvider> provider : getProviders().entrySet()) {
123                        addProvider(provider.getValue(), provider.getKey());
124                }
125                List<BaseMethodBinding<?>> serverBindings = new ArrayList<BaseMethodBinding<?>>();
126                for (ResourceBinding baseMethodBinding : myResourceNameToBinding.values()) {
127                        serverBindings.addAll(baseMethodBinding.getMethodBindings());
128                }
129                serverConfiguration.setServerBindings(serverBindings);
130                serverConfiguration.setResourceBindings(new LinkedList<ResourceBinding>(myResourceNameToBinding.values()));
131                HardcodedServerAddressStrategy hardcodedServerAddressStrategy = new HardcodedServerAddressStrategy();
132                hardcodedServerAddressStrategy.setValue(getBaseForServer());
133                serverConfiguration.setServerAddressStrategy(hardcodedServerAddressStrategy);
134                FhirVersionEnum fhirContextVersion = super.getFhirContext().getVersion().getVersion();
135                switch (fhirContextVersion) {
136                        case R4:
137                                org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider r4ServerCapabilityStatementProvider = new org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider(serverConfiguration);
138                                r4ServerCapabilityStatementProvider.initializeOperations();
139                                myR4CapabilityStatement = r4ServerCapabilityStatementProvider.getServerConformance(null);
140                                break;
141                        case DSTU3:
142                                org.hl7.fhir.dstu3.hapi.rest.server.ServerCapabilityStatementProvider dstu3ServerCapabilityStatementProvider = new org.hl7.fhir.dstu3.hapi.rest.server.ServerCapabilityStatementProvider(serverConfiguration);
143                                dstu3ServerCapabilityStatementProvider.initializeOperations();
144                                myDstu3CapabilityStatement = dstu3ServerCapabilityStatementProvider.getServerConformance(null);
145                                break;
146                        case DSTU2_1:
147                                org.hl7.fhir.dstu2016may.hapi.rest.server.ServerConformanceProvider dstu2_1ServerConformanceProvider = new org.hl7.fhir.dstu2016may.hapi.rest.server.ServerConformanceProvider(serverConfiguration);
148                                dstu2_1ServerConformanceProvider.initializeOperations();
149                                myDstu2_1Conformance = dstu2_1ServerConformanceProvider.getServerConformance(null);
150                                break;
151                        case DSTU2_HL7ORG:
152                                org.hl7.fhir.instance.conf.ServerConformanceProvider dstu2Hl7OrgServerConformanceProvider = new org.hl7.fhir.instance.conf.ServerConformanceProvider(serverConfiguration);
153                                dstu2Hl7OrgServerConformanceProvider.initializeOperations();
154                                myDstu2Hl7OrgConformance = dstu2Hl7OrgServerConformanceProvider.getServerConformance(null);
155                                break;
156                        case DSTU2:
157                                ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider dstu2ServerConformanceProvider = new ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider(serverConfiguration);
158                                dstu2ServerConformanceProvider.initializeOperations();
159                                myDstu2Conformance = dstu2ServerConformanceProvider.getServerConformance(null);
160                                break;
161                        default:
162                                throw new ConfigurationException("Unsupported Fhir version: " + fhirContextVersion);
163                }
164
165                myInitialized = true;
166        }
167
168        /**
169         * This method must return all the resource providers which need to be included in the conformance
170         * 
171         * @return a map of the resource providers and their corresponding classes. This class needs to be given explicitly because retrieving the interface using {@link Object#getClass()} may not give the
172         *         correct interface in a jee environment.
173         */
174        protected abstract ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> getProviders();
175
176        /**
177         * This method will retrieve the conformance using the http OPTIONS method
178         * 
179         * @return the response containing the conformance
180         */
181        @OPTIONS
182        @Path("/metadata")
183        public Response conformanceUsingOptions() throws IOException {
184                return conformance();
185        }
186
187        /**
188         * This method will retrieve the conformance using the http GET method
189         * 
190         * @return the response containing the conformance
191         */
192        @GET
193        @Path("/metadata")
194        public Response conformance() throws IOException {
195                setUpPostConstruct();
196
197                Builder request = getRequest(RequestTypeEnum.OPTIONS, RestOperationTypeEnum.METADATA);
198                IRestfulResponse response = request.build().getResponse();
199                response.addHeader(Constants.HEADER_CORS_ALLOW_ORIGIN, "*");
200                
201                IBaseResource conformance;
202                FhirVersionEnum fhirContextVersion = super.getFhirContext().getVersion().getVersion();
203                switch (fhirContextVersion) {
204                        case R4:
205                                conformance = myR4CapabilityStatement;
206                                break;
207                        case DSTU3:
208                                conformance = myDstu3CapabilityStatement;
209                                break;
210                        case DSTU2_1:
211                                conformance = myDstu2_1Conformance;
212                                break;
213                        case DSTU2_HL7ORG:
214                                conformance = myDstu2Hl7OrgConformance;
215                                break;
216                        case DSTU2:
217                                conformance = myDstu2Conformance;
218                                break;
219                        default:
220                                throw new ConfigurationException("Unsupported Fhir version: " + fhirContextVersion);
221                }
222                
223                if (conformance != null) {
224                        Set<SummaryEnum> summaryMode = Collections.emptySet();
225                        return (Response) response.streamResponseAsResource(conformance, false, summaryMode, Constants.STATUS_HTTP_200_OK, null, true, false);
226                }
227                return (Response) response.returnResponse(null, Constants.STATUS_HTTP_500_INTERNAL_ERROR, true, null, getResourceType().getSimpleName());
228        }
229
230        /**
231         * This method will add a provider to the conformance. This method is almost an exact copy of {@link ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods }
232         * 
233         * @param theProvider
234         *           an instance of the provider interface
235         * @param theProviderInterface
236         *           the class describing the providers interface
237         * @return the numbers of basemethodbindings added
238         * @see ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods
239         */
240        public int addProvider(IResourceProvider theProvider, Class<? extends IResourceProvider> theProviderInterface) throws ConfigurationException {
241                int count = 0;
242
243                for (Method m : ReflectionUtil.getDeclaredMethods(theProviderInterface)) {
244                        BaseMethodBinding<?> foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
245                        if (foundMethodBinding == null) {
246                                continue;
247                        }
248
249                        count++;
250
251                        // if (foundMethodBinding instanceof ConformanceMethodBinding) {
252                        // myServerConformanceMethod = foundMethodBinding;
253                        // continue;
254                        // }
255
256                        if (!Modifier.isPublic(m.getModifiers())) {
257                                throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public");
258                        } else {
259                                if (Modifier.isStatic(m.getModifiers())) {
260                                        throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static");
261                                } else {
262                                        ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName());
263
264                                        String resourceName = foundMethodBinding.getResourceName();
265                                        ResourceBinding resourceBinding;
266                                        if (resourceName == null) {
267                                                resourceBinding = myServerBinding;
268                                        } else {
269                                                RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName);
270                                                if (myResourceNameToBinding.containsKey(definition.getName())) {
271                                                        resourceBinding = myResourceNameToBinding.get(definition.getName());
272                                                } else {
273                                                        resourceBinding = new ResourceBinding();
274                                                        resourceBinding.setResourceName(resourceName);
275                                                        myResourceNameToBinding.put(resourceName, resourceBinding);
276                                                }
277                                        }
278
279                                        List<Class<?>> allowableParams = foundMethodBinding.getAllowableParamAnnotations();
280                                        if (allowableParams != null) {
281                                                for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) {
282                                                        for (Annotation annotation : nextParamAnnotations) {
283                                                                Package pack = annotation.annotationType().getPackage();
284                                                                if (pack.equals(IdParam.class.getPackage())) {
285                                                                        if (!allowableParams.contains(annotation.annotationType())) {
286                                                                                throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation);
287                                                                        }
288                                                                }
289                                                        }
290                                                }
291                                        }
292
293                                        resourceBinding.addMethod(foundMethodBinding);
294                                        ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName());
295                                }
296                        }
297                }
298
299                return count;
300        }
301
302        @SuppressWarnings("unchecked")
303        @Override
304        public Class<IBaseResource> getResourceType() {
305                FhirVersionEnum fhirContextVersion = super.getFhirContext().getVersion().getVersion();
306                switch (fhirContextVersion) {
307                        case R4:
308                                return Class.class.cast(org.hl7.fhir.r4.model.CapabilityStatement.class);
309                        case DSTU3:
310                                return Class.class.cast(org.hl7.fhir.dstu3.model.CapabilityStatement.class);
311                        case DSTU2_1:
312                                return Class.class.cast(org.hl7.fhir.dstu2016may.model.Conformance.class);
313                        case DSTU2_HL7ORG:
314                                return Class.class.cast(org.hl7.fhir.instance.model.Conformance.class);
315                        case DSTU2:
316                                return Class.class.cast(ca.uhn.fhir.model.dstu2.resource.Conformance.class);
317                        default:
318                                throw new ConfigurationException("Unsupported Fhir version: " + fhirContextVersion);
319                }
320        }
321
322}