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}