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