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}