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