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}