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