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