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