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