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