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.interceptor.api.Hook; 023import ca.uhn.fhir.interceptor.api.Pointcut; 024import ca.uhn.fhir.rest.api.server.RequestDetails; 025import jakarta.annotation.Nonnull; 026import org.apache.commons.lang3.Validate; 027 028import java.io.IOException; 029import java.io.Writer; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.function.Consumer; 033 034/** 035 * This interceptor captures and makes 036 * available the number of characters written (pre-compression if Gzip compression is being used) to the HTTP response 037 * stream for FHIR responses. 038 * <p> 039 * Response details are made available in the request {@link RequestDetails#getUserData() RequestDetails UserData map} 040 * with {@link #RESPONSE_RESULT_KEY} as the key. 041 * </p> 042 * 043 * @since 5.0.0 044 */ 045public class ResponseSizeCapturingInterceptor { 046 047 /** 048 * If the response was a character stream, a character count will be placed in the 049 * {@link RequestDetails#getUserData() RequestDetails UserData map} with this key, containing 050 * an {@link Result} value. 051 * <p> 052 * The value will be placed at the start of the {@link Pointcut#SERVER_PROCESSING_COMPLETED} pointcut, so it will not 053 * be available before that time. 054 * </p> 055 */ 056 public static final String RESPONSE_RESULT_KEY = 057 ResponseSizeCapturingInterceptor.class.getName() + "_RESPONSE_RESULT_KEY"; 058 059 private static final String COUNTING_WRITER_KEY = 060 ResponseSizeCapturingInterceptor.class.getName() + "_COUNTING_WRITER_KEY"; 061 private final List<Consumer<Result>> myConsumers = new ArrayList<>(); 062 063 @Hook(Pointcut.SERVER_OUTGOING_WRITER_CREATED) 064 public Writer capture(RequestDetails theRequestDetails, Writer theWriter) { 065 CountingWriter retVal = new CountingWriter(theWriter); 066 theRequestDetails.getUserData().put(COUNTING_WRITER_KEY, retVal); 067 return retVal; 068 } 069 070 @Hook( 071 value = Pointcut.SERVER_PROCESSING_COMPLETED, 072 order = InterceptorOrders.RESPONSE_SIZE_CAPTURING_INTERCEPTOR_COMPLETED) 073 public void completed(RequestDetails theRequestDetails) { 074 CountingWriter countingWriter = 075 (CountingWriter) theRequestDetails.getUserData().get(COUNTING_WRITER_KEY); 076 if (countingWriter != null) { 077 int charCount = countingWriter.getCount(); 078 Result result = new Result(theRequestDetails, charCount); 079 notifyConsumers(result); 080 081 theRequestDetails.getUserData().put(RESPONSE_RESULT_KEY, result); 082 } 083 } 084 085 /** 086 * Registers a new consumer. All consumers will be notified each time a request is complete. 087 * 088 * @param theConsumer The consumer 089 */ 090 public void registerConsumer(@Nonnull Consumer<Result> theConsumer) { 091 Validate.notNull(theConsumer); 092 myConsumers.add(theConsumer); 093 } 094 095 private void notifyConsumers(Result theResult) { 096 myConsumers.forEach(t -> t.accept(theResult)); 097 } 098 099 /** 100 * Contains the results of the capture 101 */ 102 public static class Result { 103 private final int myWrittenChars; 104 105 public RequestDetails getRequestDetails() { 106 return myRequestDetails; 107 } 108 109 private final RequestDetails myRequestDetails; 110 111 public Result(RequestDetails theRequestDetails, int theWrittenChars) { 112 myRequestDetails = theRequestDetails; 113 myWrittenChars = theWrittenChars; 114 } 115 116 public int getWrittenChars() { 117 return myWrittenChars; 118 } 119 } 120 121 private static class CountingWriter extends Writer { 122 123 private final Writer myWrap; 124 private int myCount; 125 126 private CountingWriter(Writer theWrap) { 127 myWrap = theWrap; 128 } 129 130 @Override 131 public void write(char[] cbuf, int off, int len) throws IOException { 132 myCount += len; 133 myWrap.write(cbuf, off, len); 134 } 135 136 @Override 137 public void flush() throws IOException { 138 myWrap.flush(); 139 } 140 141 @Override 142 public void close() throws IOException { 143 myWrap.close(); 144 } 145 146 public int getCount() { 147 return myCount; 148 } 149 } 150}