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