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}