001package ca.uhn.fhir.rest.server.interceptor;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2022 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.interceptor.api.Hook;
026import ca.uhn.fhir.interceptor.api.Interceptor;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.rest.api.Constants;
029import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
030import ca.uhn.fhir.rest.api.server.RequestDetails;
031import ca.uhn.fhir.rest.server.RestfulServer;
032import ca.uhn.fhir.rest.server.RestfulServerUtils;
033import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.hl7.fhir.instance.model.api.IPrimitiveType;
037
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040import java.io.IOException;
041import java.util.Collections;
042import java.util.HashSet;
043import java.util.Optional;
044import java.util.Set;
045
046import static org.apache.commons.lang3.StringUtils.isBlank;
047
048/**
049 * This interceptor allows a client to request that a Media resource be
050 * served as the raw contents of the resource, assuming either:
051 * <ul>
052 * <li>The client explicitly requests the correct content type using the Accept header</li>
053 * <li>The client explicitly requests raw output by adding the parameter <code>_output=data</code></li>
054 * </ul>
055 */
056@Interceptor
057public class ServeMediaResourceRawInterceptor {
058
059        public static final String MEDIA_CONTENT_CONTENT_TYPE_OPT = "Media.content.contentType";
060
061        private static final Set<RestOperationTypeEnum> RESPOND_TO_OPERATION_TYPES;
062
063        static {
064                Set<RestOperationTypeEnum> respondToOperationTypes = new HashSet<>();
065                respondToOperationTypes.add(RestOperationTypeEnum.READ);
066                respondToOperationTypes.add(RestOperationTypeEnum.VREAD);
067                RESPOND_TO_OPERATION_TYPES = Collections.unmodifiableSet(respondToOperationTypes);
068        }
069
070        @Hook(value=Pointcut.SERVER_OUTGOING_RESPONSE, order = InterceptorOrders.SERVE_MEDIA_RESOURCE_RAW_INTERCEPTOR)
071        public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException {
072                if (theResponseObject == null) {
073                        return true;
074                }
075
076
077                FhirContext context = theRequestDetails.getFhirContext();
078                String resourceName = context.getResourceType(theResponseObject);
079
080                // Are we serving a FHIR read request on the Media resource type
081                if (!"Media".equals(resourceName) || !RESPOND_TO_OPERATION_TYPES.contains(theRequestDetails.getRestOperationType())) {
082                        return true;
083                }
084
085                // What is the content type of the Media resource we're returning?
086                String contentType = null;
087                Optional<IPrimitiveType> contentTypeOpt = context.newFluentPath().evaluateFirst(theResponseObject, MEDIA_CONTENT_CONTENT_TYPE_OPT, IPrimitiveType.class);
088                if (contentTypeOpt.isPresent()) {
089                        contentType = contentTypeOpt.get().getValueAsString();
090                }
091
092                // What is the data of the Media resource we're returning?
093                IPrimitiveType<byte[]> data = null;
094                Optional<IPrimitiveType> dataOpt = context.newFluentPath().evaluateFirst(theResponseObject, "Media.content.data", IPrimitiveType.class);
095                if (dataOpt.isPresent()) {
096                        data = dataOpt.get();
097                }
098
099                if (isBlank(contentType) || data == null) {
100                        return true;
101                }
102
103                RestfulServerUtils.ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, null, contentType);
104                if (responseEncoding != null) {
105                        if (contentType.equals(responseEncoding.getContentType())) {
106                                returnRawResponse(theRequestDetails, theServletResponse, contentType, data);
107                                return false;
108
109                        }
110                }
111
112                String[] outputParam = theRequestDetails.getParameters().get("_output");
113                if (outputParam != null && "data".equals(outputParam[0])) {
114                        returnRawResponse(theRequestDetails, theServletResponse, contentType, data);
115                        return false;
116                }
117
118                return true;
119        }
120
121        private void returnRawResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, String theContentType, IPrimitiveType<byte[]> theData) {
122                theServletResponse.setStatus(200);
123                if (theRequestDetails.getServer() instanceof RestfulServer) {
124                        RestfulServer rs = (RestfulServer) theRequestDetails.getServer();
125                        rs.addHeadersToResponse(theServletResponse);
126                }
127
128                theServletResponse.addHeader(Constants.HEADER_CONTENT_TYPE, theContentType);
129
130                // Write the response
131                try {
132                        theServletResponse.getOutputStream().write(theData.getValue());
133                        theServletResponse.getOutputStream().close();
134                } catch (IOException e) {
135                        throw new InternalErrorException(Msg.code(321) + e);
136                }
137        }
138}