001package ca.uhn.fhir.rest.server.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Server 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 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.RestOperationTypeEnum;
030import ca.uhn.fhir.rest.api.server.RequestDetails;
031import ca.uhn.fhir.rest.server.IResourceProvider;
032import ca.uhn.fhir.rest.server.RestfulServerUtils;
033import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
034import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
035import ca.uhn.fhir.util.BinaryUtil;
036import org.apache.commons.io.IOUtils;
037import org.apache.commons.lang3.Validate;
038import org.hl7.fhir.instance.model.api.IBaseBinary;
039import org.hl7.fhir.instance.model.api.IBaseResource;
040
041import javax.annotation.Nonnull;
042import java.io.ByteArrayInputStream;
043import java.io.IOException;
044import java.io.InputStreamReader;
045import java.io.Reader;
046import java.lang.reflect.Method;
047import java.lang.reflect.Modifier;
048import java.nio.charset.Charset;
049import java.util.Collection;
050
051import static org.apache.commons.lang3.StringUtils.isBlank;
052import static org.apache.commons.lang3.StringUtils.isNotBlank;
053
054public class ResourceParameter implements IParameter {
055
056        private final boolean myMethodIsOperation;
057        private Mode myMode;
058        private Class<? extends IBaseResource> myResourceType;
059
060        public ResourceParameter(Class<? extends IBaseResource> theParameterType, Object theProvider, Mode theMode, boolean theMethodIsOperation) {
061                Validate.notNull(theParameterType, "theParameterType can not be null");
062                Validate.notNull(theMode, "theMode can not be null");
063
064                myResourceType = theParameterType;
065                myMode = theMode;
066                myMethodIsOperation = theMethodIsOperation;
067
068                Class<? extends IBaseResource> providerResourceType = null;
069                if (theProvider instanceof IResourceProvider) {
070                        providerResourceType = ((IResourceProvider) theProvider).getResourceType();
071                }
072
073                if (Modifier.isAbstract(myResourceType.getModifiers()) && providerResourceType != null) {
074                        myResourceType = providerResourceType;
075                }
076
077        }
078
079        public Mode getMode() {
080                return myMode;
081        }
082
083        public Class<? extends IBaseResource> getResourceType() {
084                return myResourceType;
085        }
086
087        @Override
088        public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
089                // ignore for now
090        }
091
092
093        @Override
094        public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
095                switch (myMode) {
096                        case BODY:
097                                try {
098                                        return IOUtils.toString(createRequestReader(theRequest));
099                                } catch (IOException e) {
100                                        // Shouldn't happen since we're reading from a byte array
101                                        throw new InternalErrorException("Failed to load request", e);
102                                }
103                        case BODY_BYTE_ARRAY:
104                                return theRequest.loadRequestContents();
105                        case ENCODING:
106                                return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest);
107                        case RESOURCE:
108                        default:
109                                Class<? extends IBaseResource> resourceTypeToParse = myResourceType;
110                                if (myMethodIsOperation) {
111                                        // Operations typically have a Parameters resource as the body
112                                        resourceTypeToParse = null;
113                                }
114                                return parseResourceFromRequest(theRequest, theMethodBinding, resourceTypeToParse);
115                }
116                // }
117        }
118
119        public enum Mode {
120                BODY, BODY_BYTE_ARRAY, ENCODING, RESOURCE
121        }
122
123        public static Reader createRequestReader(RequestDetails theRequest, Charset charset) {
124                Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset);
125                return requestReader;
126        }
127
128        public static Reader createRequestReader(RequestDetails theRequest) {
129                return createRequestReader(theRequest, determineRequestCharset(theRequest));
130        }
131
132        public static Charset determineRequestCharset(RequestDetails theRequest) {
133                Charset charset = theRequest.getCharset();
134                if (charset == null) {
135                        charset = Charset.forName("UTF-8");
136                }
137                return charset;
138        }
139
140        @SuppressWarnings("unchecked")
141        public static <T extends IBaseResource> T loadResourceFromRequest(RequestDetails theRequest, @Nonnull BaseMethodBinding<?> theMethodBinding, Class<T> theResourceType) {
142                FhirContext ctx = theRequest.getServer().getFhirContext();
143
144                final Charset charset = determineRequestCharset(theRequest);
145                Reader requestReader = createRequestReader(theRequest, charset);
146
147                RestOperationTypeEnum restOperationType = theMethodBinding != null ? theMethodBinding.getRestOperationType() : null;
148
149                EncodingEnum encoding = RestfulServerUtils.determineRequestEncodingNoDefault(theRequest);
150                if (encoding == null) {
151                        String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
152                        if (ctValue != null) {
153                                if (ctValue.startsWith("application/x-www-form-urlencoded")) {
154                                        String msg = theRequest.getServer().getFhirContext().getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, theMethodBinding.getRestOperationType());
155                                        throw new InvalidRequestException(msg);
156                                }
157                        }
158                        if (isBlank(ctValue)) {
159                                /*
160                                 * If the client didn't send a content type, try to guess
161                                 */
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(e);
168                                }
169                                if (isBlank(body)) {
170                                        return null;
171                                }
172                                encoding = EncodingEnum.detectEncodingNoDefault(body);
173                                if (encoding == null) {
174                                        String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType);
175                                        throw new InvalidRequestException(msg);
176                                }
177                                requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset);
178                        } else {
179                                String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, restOperationType);
180                                throw new InvalidRequestException(msg);
181                        }
182                }
183
184                IParser parser = encoding.newParser(ctx);
185                parser.setServerBaseUrl(theRequest.getFhirServerBase());
186                T retVal;
187                try {
188                        if (theResourceType != null) {
189                                retVal = parser.parseResource(theResourceType, requestReader);
190                        } else {
191                                retVal = (T) parser.parseResource(requestReader);
192                        }
193                } catch (DataFormatException e) {
194                        String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "failedToParseRequest", encoding.name(), e.getMessage());
195                        throw new InvalidRequestException(msg);
196                }
197
198                return retVal;
199        }
200
201        public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding, Class<? extends IBaseResource> theResourceType) {
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                if (retVal == null) {
228                        retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType);
229                }
230                return retVal;
231        }
232
233}