001/* 002 * #%L 003 * HAPI FHIR - Server Framework 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.rest.server.method; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.api.HookParams; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.model.valueset.BundleTypeEnum; 028import ca.uhn.fhir.rest.annotation.Metadata; 029import ca.uhn.fhir.rest.api.CacheControlDirective; 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.server.IBundleProvider; 034import ca.uhn.fhir.rest.api.server.IRestfulServer; 035import ca.uhn.fhir.rest.api.server.RequestDetails; 036import ca.uhn.fhir.rest.server.RestfulServer; 037import ca.uhn.fhir.rest.server.SimpleBundleProvider; 038import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 039import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 040import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 041import ca.uhn.fhir.system.HapiSystemProperties; 042import jakarta.annotation.Nonnull; 043import org.hl7.fhir.instance.model.api.IBaseConformance; 044 045import java.lang.reflect.Method; 046import java.util.concurrent.ExecutorService; 047import java.util.concurrent.LinkedBlockingQueue; 048import java.util.concurrent.ThreadFactory; 049import java.util.concurrent.ThreadPoolExecutor; 050import java.util.concurrent.TimeUnit; 051import java.util.concurrent.atomic.AtomicLong; 052import java.util.concurrent.atomic.AtomicReference; 053 054public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding { 055 public static final String CACHE_THREAD_PREFIX = "capabilitystatement-cache-"; 056 /* 057 * Note: This caching mechanism should probably be configurable and maybe 058 * even applicable to other bindings. It's particularly important for this 059 * operation though, so a one-off is fine for now 060 */ 061 private final AtomicReference<IBaseConformance> myCachedResponse = new AtomicReference<>(); 062 private final AtomicLong myCachedResponseExpires = new AtomicLong(0L); 063 private final ExecutorService myThreadPool; 064 private long myCacheMillis = 60 * 1000; 065 066 ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { 067 super(theMethod.getReturnType(), theMethod, theContext, theProvider); 068 069 MethodReturnTypeEnum methodReturnType = getMethodReturnType(); 070 Class<?> genericReturnType = (Class<?>) theMethod.getGenericReturnType(); 071 if (methodReturnType != MethodReturnTypeEnum.RESOURCE 072 || !IBaseConformance.class.isAssignableFrom(genericReturnType)) { 073 throw new ConfigurationException( 074 Msg.code(387) + "Conformance resource provider method '" + theMethod.getName() 075 + "' should return a Conformance resource class, returns: " + theMethod.getReturnType()); 076 } 077 078 Metadata metadata = theMethod.getAnnotation(Metadata.class); 079 if (metadata != null) { 080 setCacheMillis(metadata.cacheMillis()); 081 } 082 083 ThreadFactory threadFactory = r -> { 084 Thread t = new Thread(r); 085 t.setName(CACHE_THREAD_PREFIX + t.getId()); 086 t.setDaemon(false); 087 return t; 088 }; 089 myThreadPool = new ThreadPoolExecutor( 090 1, 091 1, 092 0L, 093 TimeUnit.MILLISECONDS, 094 new LinkedBlockingQueue<>(1), 095 threadFactory, 096 new ThreadPoolExecutor.DiscardOldestPolicy()); 097 } 098 099 /** 100 * Returns the number of milliseconds to cache the generated CapabilityStatement for. Default is one minute, and can be 101 * set to 0 to never cache. 102 * 103 * @see #setCacheMillis(long) 104 * @see Metadata#cacheMillis() 105 * @since 4.1.0 106 */ 107 private long getCacheMillis() { 108 return myCacheMillis; 109 } 110 111 /** 112 * Returns the number of milliseconds to cache the generated CapabilityStatement for. Default is one minute, and can be 113 * set to 0 to never cache. 114 * 115 * @see #getCacheMillis() 116 * @see Metadata#cacheMillis() 117 * @since 4.1.0 118 */ 119 public void setCacheMillis(long theCacheMillis) { 120 myCacheMillis = theCacheMillis; 121 } 122 123 @Override 124 public ReturnTypeEnum getReturnType() { 125 return ReturnTypeEnum.RESOURCE; 126 } 127 128 @Override 129 public void close() { 130 super.close(); 131 132 myThreadPool.shutdown(); 133 } 134 135 @Override 136 public IBundleProvider invokeServer( 137 IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) 138 throws BaseServerResponseException { 139 IBaseConformance conf; 140 141 CacheControlDirective cacheControlDirective = 142 new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)); 143 144 if (cacheControlDirective.isNoCache()) conf = null; 145 else { 146 conf = myCachedResponse.get(); 147 if (HapiSystemProperties.isTestModeEnabled()) { 148 conf = null; 149 } 150 if (conf != null) { 151 long expires = myCachedResponseExpires.get(); 152 if (expires < System.currentTimeMillis()) { 153 myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis()); 154 myThreadPool.submit(() -> createCapabilityStatement(theRequest, theMethodParams)); 155 } 156 } 157 } 158 if (conf != null) { 159 // Handle server action interceptors 160 RestOperationTypeEnum operationType = getRestOperationType(theRequest); 161 if (operationType != null) { 162 163 populateRequestDetailsForInterceptor(theRequest, theMethodParams); 164 165 // Interceptor hook: SERVER_INCOMING_REQUEST_PRE_HANDLED 166 if (theRequest.getInterceptorBroadcaster() != null) { 167 HookParams preHandledParams = new HookParams(); 168 preHandledParams.add(RestOperationTypeEnum.class, theRequest.getRestOperationType()); 169 preHandledParams.add(RequestDetails.class, theRequest); 170 preHandledParams.addIfMatchesType(ServletRequestDetails.class, theRequest); 171 theRequest 172 .getInterceptorBroadcaster() 173 .callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, preHandledParams); 174 } 175 } 176 } 177 178 if (conf == null) { 179 conf = createCapabilityStatement(theRequest, theMethodParams); 180 } 181 182 return new SimpleBundleProvider(conf); 183 } 184 185 private IBaseConformance createCapabilityStatement(RequestDetails theRequest, Object[] theMethodParams) { 186 IBaseConformance conf = (IBaseConformance) invokeServerMethod(theRequest, theMethodParams); 187 188 // Interceptor hook: SERVER_CAPABILITY_STATEMENT_GENERATED 189 if (theRequest.getInterceptorBroadcaster() != null) { 190 HookParams params = new HookParams(); 191 params.add(IBaseConformance.class, conf); 192 params.add(RequestDetails.class, theRequest); 193 params.addIfMatchesType(ServletRequestDetails.class, theRequest); 194 IBaseConformance outcome = (IBaseConformance) theRequest 195 .getInterceptorBroadcaster() 196 .callHooksAndReturnObject(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED, params); 197 if (outcome != null) { 198 conf = outcome; 199 } 200 } 201 202 if (myCacheMillis > 0) { 203 myCachedResponse.set(conf); 204 myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis()); 205 } 206 207 return conf; 208 } 209 210 @Override 211 public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { 212 if (theRequest.getRequestType() == RequestTypeEnum.OPTIONS) { 213 if (theRequest.getOperation() == null && theRequest.getResourceName() == null) { 214 return MethodMatchEnum.EXACT; 215 } 216 } 217 218 if (theRequest.getResourceName() != null) { 219 return MethodMatchEnum.NONE; 220 } 221 222 if ("metadata".equals(theRequest.getOperation())) { 223 if (theRequest.getRequestType() == RequestTypeEnum.GET) { 224 return MethodMatchEnum.EXACT; 225 } 226 throw new MethodNotAllowedException( 227 Msg.code(388) + "/metadata request must use HTTP GET", RequestTypeEnum.GET); 228 } 229 230 return MethodMatchEnum.NONE; 231 } 232 233 @Nonnull 234 @Override 235 public RestOperationTypeEnum getRestOperationType() { 236 return RestOperationTypeEnum.METADATA; 237 } 238 239 @Override 240 protected BundleTypeEnum getResponseBundleType() { 241 return null; 242 } 243 244 /** 245 * Create and return the server's CapabilityStatement 246 */ 247 public IBaseConformance provideCapabilityStatement(RestfulServer theServer, RequestDetails theRequest) { 248 Object[] params = createMethodParams(theRequest); 249 IBundleProvider resultObj = invokeServer(theServer, theRequest, params); 250 return (IBaseConformance) resultObj.getResources(0, 1).get(0); 251 } 252}