001/* 002 * #%L 003 * HAPI FHIR - Server 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.server.method; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.FhirVersionEnum; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.parser.DataFormatException; 026import ca.uhn.fhir.parser.IParser; 027import ca.uhn.fhir.rest.api.Constants; 028import ca.uhn.fhir.rest.api.EncodingEnum; 029import ca.uhn.fhir.rest.api.RequestTypeEnum; 030import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 031import ca.uhn.fhir.rest.api.server.RequestDetails; 032import ca.uhn.fhir.rest.server.IResourceProvider; 033import ca.uhn.fhir.rest.server.RestfulServerUtils; 034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 035import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 036import ca.uhn.fhir.util.BinaryUtil; 037import jakarta.annotation.Nonnull; 038import org.apache.commons.io.IOUtils; 039import org.apache.commons.lang3.Validate; 040import org.hl7.fhir.instance.model.api.IBaseBinary; 041import org.hl7.fhir.instance.model.api.IBaseResource; 042 043import java.io.ByteArrayInputStream; 044import java.io.IOException; 045import java.io.InputStreamReader; 046import java.io.Reader; 047import java.lang.reflect.Method; 048import java.lang.reflect.Modifier; 049import java.nio.charset.Charset; 050import java.util.Collection; 051 052import static org.apache.commons.lang3.StringUtils.isBlank; 053import static org.apache.commons.lang3.StringUtils.isNotBlank; 054 055public class ResourceParameter implements IParameter { 056 057 private final boolean myMethodIsOperationOrPatch; 058 private Mode myMode; 059 private Class<? extends IBaseResource> myResourceType; 060 061 public ResourceParameter( 062 Class<? extends IBaseResource> theParameterType, 063 Object theProvider, 064 Mode theMode, 065 boolean theMethodIsOperation, 066 boolean theMethodIsPatch) { 067 Validate.notNull(theParameterType, "theParameterType can not be null"); 068 Validate.notNull(theMode, "theMode can not be null"); 069 070 myResourceType = theParameterType; 071 myMode = theMode; 072 myMethodIsOperationOrPatch = theMethodIsOperation || theMethodIsPatch; 073 074 Class<? extends IBaseResource> providerResourceType = null; 075 if (theProvider instanceof IResourceProvider) { 076 providerResourceType = ((IResourceProvider) theProvider).getResourceType(); 077 } 078 079 if (Modifier.isAbstract(myResourceType.getModifiers()) && providerResourceType != null) { 080 myResourceType = providerResourceType; 081 } 082 } 083 084 public Mode getMode() { 085 return myMode; 086 } 087 088 public Class<? extends IBaseResource> getResourceType() { 089 return myResourceType; 090 } 091 092 @Override 093 public void initializeTypes( 094 Method theMethod, 095 Class<? extends Collection<?>> theOuterCollectionType, 096 Class<? extends Collection<?>> theInnerCollectionType, 097 Class<?> theParameterType) { 098 // ignore for now 099 } 100 101 @Override 102 public Object translateQueryParametersIntoServerArgument( 103 RequestDetails theRequest, BaseMethodBinding theMethodBinding) 104 throws InternalErrorException, InvalidRequestException { 105 switch (myMode) { 106 case BODY: 107 try { 108 return IOUtils.toString(createRequestReader(theRequest)); 109 } catch (IOException e) { 110 // Shouldn't happen since we're reading from a byte array 111 throw new InternalErrorException(Msg.code(445) + "Failed to load request", e); 112 } 113 case BODY_BYTE_ARRAY: 114 return theRequest.loadRequestContents(); 115 case ENCODING: 116 return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest); 117 case RESOURCE: 118 default: 119 Class<? extends IBaseResource> resourceTypeToParse = myResourceType; 120 if (myMethodIsOperationOrPatch) { 121 // Operations typically have a Parameters resource as the body 122 resourceTypeToParse = null; 123 } 124 return parseResourceFromRequest(theRequest, theMethodBinding, resourceTypeToParse); 125 } 126 // } 127 } 128 129 public enum Mode { 130 BODY, 131 BODY_BYTE_ARRAY, 132 ENCODING, 133 RESOURCE 134 } 135 136 private static Reader createRequestReader(RequestDetails theRequest, Charset charset) { 137 return new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset); 138 } 139 140 // Do not make private 141 @SuppressWarnings("WeakerAccess") 142 public static Reader createRequestReader(RequestDetails theRequest) { 143 return createRequestReader(theRequest, determineRequestCharset(theRequest)); 144 } 145 146 public static Charset determineRequestCharset(RequestDetails theRequest) { 147 Charset charset = theRequest.getCharset(); 148 if (charset == null) { 149 charset = Charset.forName("UTF-8"); 150 } 151 return charset; 152 } 153 154 @SuppressWarnings("unchecked") 155 static <T extends IBaseResource> T loadResourceFromRequest( 156 RequestDetails theRequest, @Nonnull BaseMethodBinding theMethodBinding, Class<T> theResourceType) { 157 FhirContext ctx = theRequest.getServer().getFhirContext(); 158 159 final Charset charset = determineRequestCharset(theRequest); 160 Reader requestReader = createRequestReader(theRequest, charset); 161 162 RestOperationTypeEnum restOperationType = 163 theMethodBinding != null ? theMethodBinding.getRestOperationType() : null; 164 165 EncodingEnum encoding = RestfulServerUtils.determineRequestEncodingNoDefault(theRequest); 166 if (encoding == null) { 167 String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); 168 if (ctValue != null) { 169 if (ctValue.startsWith("application/x-www-form-urlencoded")) { 170 String msg = theRequest 171 .getServer() 172 .getFhirContext() 173 .getLocalizer() 174 .getMessage( 175 ResourceParameter.class, 176 "invalidContentTypeInRequest", 177 ctValue, 178 theMethodBinding.getRestOperationType()); 179 throw new InvalidRequestException(Msg.code(446) + msg); 180 } 181 } 182 if (isBlank(ctValue)) { 183 String body; 184 try { 185 body = IOUtils.toString(requestReader); 186 } catch (IOException e) { 187 // This shouldn't happen since we're reading from a byte array.. 188 throw new InternalErrorException(Msg.code(447) + e); 189 } 190 if (isBlank(body)) { 191 return null; 192 } 193 194 String msg = ctx.getLocalizer() 195 .getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType); 196 throw new InvalidRequestException(Msg.code(448) + msg); 197 } else { 198 String msg = ctx.getLocalizer() 199 .getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, restOperationType); 200 throw new InvalidRequestException(Msg.code(449) + msg); 201 } 202 } 203 204 IParser parser = encoding.newParser(ctx); 205 parser.setServerBaseUrl(theRequest.getFhirServerBase()); 206 T retVal; 207 try { 208 if (theResourceType != null) { 209 retVal = parser.parseResource(theResourceType, requestReader); 210 } else { 211 retVal = (T) parser.parseResource(requestReader); 212 } 213 } catch (DataFormatException e) { 214 String msg = ctx.getLocalizer() 215 .getMessage(ResourceParameter.class, "failedToParseRequest", encoding.name(), e.getMessage()); 216 throw new InvalidRequestException(Msg.code(450) + msg); 217 } 218 219 return retVal; 220 } 221 222 static IBaseResource parseResourceFromRequest( 223 RequestDetails theRequest, 224 @Nonnull BaseMethodBinding theMethodBinding, 225 Class<? extends IBaseResource> theResourceType) { 226 if (theRequest.getResource() != null) { 227 return theRequest.getResource(); 228 } 229 230 IBaseResource retVal = null; 231 232 if (theResourceType != null && IBaseBinary.class.isAssignableFrom(theResourceType)) { 233 String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); 234 if (EncodingEnum.forContentTypeStrict(ct) == null) { 235 FhirContext ctx = theRequest.getServer().getFhirContext(); 236 IBaseBinary binary = BinaryUtil.newBinary(ctx); 237 binary.setId(theRequest.getId()); 238 binary.setContentType(ct); 239 binary.setContent(theRequest.loadRequestContents()); 240 retVal = binary; 241 242 /* 243 * Security context header, which is only in 244 * DSTU3+ 245 */ 246 if (ctx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 247 String securityContext = theRequest.getHeader(Constants.HEADER_X_SECURITY_CONTEXT); 248 if (isNotBlank(securityContext)) { 249 BinaryUtil.setSecurityContext(ctx, binary, securityContext); 250 } 251 } 252 } 253 } 254 255 boolean isNonFhirPatch = false; 256 if (theRequest.getRequestType() == RequestTypeEnum.PATCH) { 257 EncodingEnum requestEncoding = RestfulServerUtils.determineRequestEncodingNoDefault(theRequest, true); 258 if (requestEncoding == null) { 259 isNonFhirPatch = true; 260 } 261 } 262 263 if (retVal == null && !isNonFhirPatch) { 264 retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType); 265 } 266 267 theRequest.setResource(retVal); 268 269 return retVal; 270 } 271}