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