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}