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}