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