001/*- 002 * #%L 003 * HAPI FHIR - Client 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.client.method; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.model.api.IResource; 026import ca.uhn.fhir.model.valueset.BundleTypeEnum; 027import ca.uhn.fhir.parser.IParser; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.api.MethodOutcome; 030import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; 031import ca.uhn.fhir.util.BundleUtil; 032import ca.uhn.fhir.util.ReflectionUtil; 033import org.hl7.fhir.instance.model.api.IBaseBundle; 034import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 035import org.hl7.fhir.instance.model.api.IBaseResource; 036 037import java.io.IOException; 038import java.io.InputStream; 039import java.lang.reflect.Method; 040import java.lang.reflect.Modifier; 041import java.util.ArrayList; 042import java.util.Collection; 043import java.util.Collections; 044import java.util.HashSet; 045import java.util.Iterator; 046import java.util.List; 047import java.util.Map; 048import java.util.Set; 049 050public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> { 051 protected static final Set<String> ALLOWED_PARAMS; 052 private static final org.slf4j.Logger ourLog = 053 org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); 054 055 static { 056 HashSet<String> set = new HashSet<String>(); 057 set.add(Constants.PARAM_FORMAT); 058 set.add(Constants.PARAM_NARRATIVE); 059 set.add(Constants.PARAM_PRETTY); 060 set.add(Constants.PARAM_SORT); 061 set.add(Constants.PARAM_SORT_ASC); 062 set.add(Constants.PARAM_SORT_DESC); 063 set.add(Constants.PARAM_COUNT); 064 set.add(Constants.PARAM_OFFSET); 065 set.add(Constants.PARAM_SUMMARY); 066 set.add(Constants.PARAM_ELEMENTS); 067 ALLOWED_PARAMS = Collections.unmodifiableSet(set); 068 } 069 070 private MethodReturnTypeEnum myMethodReturnType; 071 private Class<?> myResourceListCollectionType; 072 private String myResourceName; 073 private Class<? extends IBaseResource> myResourceType; 074 private List<Class<? extends IBaseResource>> myPreferTypesList; 075 076 @SuppressWarnings("unchecked") 077 public BaseResourceReturningMethodBinding( 078 Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 079 super(theMethod, theContext, theProvider); 080 081 Class<?> methodReturnType = theMethod.getReturnType(); 082 if (Collection.class.isAssignableFrom(methodReturnType)) { 083 084 myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; 085 Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); 086 if (collectionType != null) { 087 if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) { 088 throw new ConfigurationException(Msg.code(1458) + "Method " 089 + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() 090 + " returns an invalid collection generic type: " + collectionType); 091 } 092 } 093 myResourceListCollectionType = collectionType; 094 095 } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) { 096 if (Modifier.isAbstract(methodReturnType.getModifiers()) == false 097 && theContext 098 .getResourceDefinition((Class<? extends IBaseResource>) methodReturnType) 099 .isBundle()) { 100 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE; 101 } else { 102 myMethodReturnType = MethodReturnTypeEnum.RESOURCE; 103 } 104 } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) { 105 myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME; 106 } else { 107 throw new ConfigurationException(Msg.code(1459) + "Invalid return type '" 108 + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " 109 + theMethod.getDeclaringClass().getCanonicalName()); 110 } 111 112 if (theReturnResourceType != null) { 113 if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { 114 if (Modifier.isAbstract(theReturnResourceType.getModifiers()) 115 || Modifier.isInterface(theReturnResourceType.getModifiers())) { 116 // If we're returning an abstract type, that's ok 117 } else { 118 myResourceType = (Class<? extends IResource>) theReturnResourceType; 119 myResourceName = theContext.getResourceType(myResourceType); 120 } 121 } 122 } 123 124 myPreferTypesList = createPreferTypesList(); 125 } 126 127 public MethodReturnTypeEnum getMethodReturnType() { 128 return myMethodReturnType; 129 } 130 131 @Override 132 public String getResourceName() { 133 return myResourceName; 134 } 135 136 /** 137 * If the response is a bundle, this type will be placed in the root of the bundle (can be null) 138 */ 139 protected abstract BundleTypeEnum getResponseBundleType(); 140 141 public abstract ReturnTypeEnum getReturnType(); 142 143 @Override 144 public Object invokeClient( 145 String theResponseMimeType, 146 InputStream theResponseInputStream, 147 int theResponseStatusCode, 148 Map<String, List<String>> theHeaders) 149 throws IOException { 150 151 if (Constants.STATUS_HTTP_204_NO_CONTENT == theResponseStatusCode) { 152 return toReturnType(null); 153 } 154 155 IParser parser = createAppropriateParserForParsingResponse( 156 theResponseMimeType, theResponseInputStream, theResponseStatusCode, myPreferTypesList); 157 158 switch (getReturnType()) { 159 case BUNDLE: { 160 IBaseBundle bundle; 161 List<? extends IBaseResource> listOfResources; 162 Class<? extends IBaseResource> type = 163 getContext().getResourceDefinition("Bundle").getImplementingClass(); 164 bundle = (IBaseBundle) parser.parseResource(type, theResponseInputStream); 165 listOfResources = BundleUtil.toListOfResources(getContext(), bundle); 166 167 switch (getMethodReturnType()) { 168 case BUNDLE_RESOURCE: 169 return bundle; 170 case LIST_OF_RESOURCES: 171 if (myResourceListCollectionType != null) { 172 for (Iterator<? extends IBaseResource> iter = listOfResources.iterator(); 173 iter.hasNext(); ) { 174 IBaseResource next = iter.next(); 175 if (!myResourceListCollectionType.isAssignableFrom(next.getClass())) { 176 ourLog.debug( 177 "Not returning resource of type {} because it is not a subclass or instance of {}", 178 next.getClass(), 179 myResourceListCollectionType); 180 iter.remove(); 181 } 182 } 183 } 184 return listOfResources; 185 case RESOURCE: 186 List<IBaseResource> list = BundleUtil.toListOfResources(getContext(), bundle); 187 if (list.size() == 0) { 188 return null; 189 } else if (list.size() == 1) { 190 return list.get(0); 191 } else { 192 throw new InvalidResponseException( 193 Msg.code(1460) 194 + "FHIR server call returned a bundle with multiple resources, but this method is only able to returns one.", 195 theResponseStatusCode); 196 } 197 default: 198 break; 199 } 200 break; 201 } 202 case RESOURCE: { 203 IBaseResource resource; 204 if (myResourceType != null) { 205 resource = parser.parseResource(myResourceType, theResponseInputStream); 206 } else { 207 resource = parser.parseResource(theResponseInputStream); 208 } 209 210 MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource); 211 212 return toReturnType(resource); 213 } 214 } 215 216 throw new IllegalStateException(Msg.code(1461) + "Should not get here!"); 217 } 218 219 private Object toReturnType(IBaseResource resource) { 220 Object retVal = null; 221 222 switch (getMethodReturnType()) { 223 case LIST_OF_RESOURCES: 224 retVal = Collections.emptyList(); 225 if (resource != null) { 226 retVal = Collections.singletonList(resource); 227 } 228 break; 229 case RESOURCE: 230 retVal = resource; 231 break; 232 case BUNDLE_RESOURCE: 233 retVal = resource; 234 break; 235 case METHOD_OUTCOME: 236 MethodOutcome outcome = new MethodOutcome(); 237 outcome.setOperationOutcome((IBaseOperationOutcome) resource); 238 retVal = outcome; 239 break; 240 } 241 return retVal; 242 } 243 244 @SuppressWarnings("unchecked") 245 private List<Class<? extends IBaseResource>> createPreferTypesList() { 246 List<Class<? extends IBaseResource>> preferTypes = null; 247 if (myResourceType != null && !BaseMethodBinding.isResourceInterface(myResourceType)) { 248 preferTypes = new ArrayList<Class<? extends IBaseResource>>(1); 249 preferTypes.add(myResourceType); 250 } else if (myResourceListCollectionType != null 251 && IBaseResource.class.isAssignableFrom(myResourceListCollectionType) 252 && !BaseMethodBinding.isResourceInterface(myResourceListCollectionType)) { 253 preferTypes = new ArrayList<Class<? extends IBaseResource>>(1); 254 preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType); 255 } 256 return preferTypes; 257 } 258 259 /** 260 * Should the response include a Content-Location header. Search method bunding (and any others?) may override this to disable the content-location, since it doesn't make sense 261 */ 262 protected boolean isAddContentLocationHeader() { 263 return true; 264 } 265 266 protected void setResourceName(String theResourceName) { 267 myResourceName = theResourceName; 268 } 269 270 public enum MethodReturnTypeEnum { 271 BUNDLE_RESOURCE, 272 LIST_OF_RESOURCES, 273 METHOD_OUTCOME, 274 RESOURCE 275 } 276 277 public static class ResourceOrDstu1Bundle { 278 279 private final IBaseResource myResource; 280 281 public ResourceOrDstu1Bundle(IBaseResource theResource) { 282 myResource = theResource; 283 } 284 285 public IBaseResource getResource() { 286 return myResource; 287 } 288 } 289 290 public enum ReturnTypeEnum { 291 BUNDLE, 292 RESOURCE 293 } 294}