001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.util;
018
019import java.io.*;
020import java.nio.charset.*;
021
022import jakarta.servlet.*;
023import jakarta.servlet.http.*;
024
025/**
026 * Wraps an {@link HttpServletResponse} and tees a <b>bounded</b> copy of the output stream into memory for debug logging.
027 *
028 * <p>
029 * At most a configured cap (default 8&nbsp;KB) of the response body is captured while all bytes are still written through
030 * to the real client. The total number of bytes written is tracked so a truncation marker can be rendered.
031 *
032 * <p>
033 * The underlying {@link HttpServletResponse#getOutputStream()} is not acquired until the first
034 * {@link #getOutputStream()} or {@link #getWriter()} call &mdash; acquiring it eagerly in the constructor would
035 * permanently lock the response into stream mode, making a subsequent direct {@code getWriter()} call on this
036 * wrapper (or on the underlying response) throw {@link IllegalStateException} per the servlet spec, even for
037 * calls that never write anything.
038 *
039 */
040@SuppressWarnings({
041   "resource" // The tee'd response stream is owned by the underlying response/servlet container, which closes it when the response completes; closing it here would break response body caching. Eclipse JDT @Owning warning is by design.
042})
043public class CachingHttpServletResponse extends HttpServletResponseWrapper {
044
045   /** Default body capture cap, in bytes (8&nbsp;KB). */
046   public static final int DEFAULT_CAP = 8 * 1024;
047
048   /**
049    * Wraps the specified response inside a {@link CachingHttpServletResponse} if it isn't already, using the default cap.
050    *
051    * @param res The response to wrap. Must not be <jk>null</jk>.
052    * @return The wrapped response.
053    */
054   public static CachingHttpServletResponse wrap(HttpServletResponse res) {
055      return wrap(res, DEFAULT_CAP);
056   }
057
058   /**
059    * Wraps the specified response inside a {@link CachingHttpServletResponse} if it isn't already.
060    *
061    * @param res The response to wrap. Must not be <jk>null</jk>.
062    * @param cap The maximum number of body bytes to capture.
063    * @return The wrapped response.
064    */
065   public static CachingHttpServletResponse wrap(HttpServletResponse res, int cap) {
066      if (res instanceof CachingHttpServletResponse res2)
067         return res2;
068      return new CachingHttpServletResponse(res, cap);
069   }
070
071   private final int cap;
072   private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
073   private long totalLength = 0;
074   private TeeServletOutputStream stream;
075   private PrintWriter writer;
076
077   /**
078    * Constructor.
079    *
080    * @param res The wrapped servlet response. Must not be <jk>null</jk>.
081    * @param cap The maximum number of body bytes to capture.
082    */
083   protected CachingHttpServletResponse(HttpServletResponse res, int cap) {
084      super(res);
085      this.cap = cap;
086   }
087
088   /**
089    * Returns the captured (possibly truncated) response body bytes.
090    *
091    * @return The captured body bytes (at most the configured cap). Never <jk>null</jk>.
092    */
093   public byte[] getContent() { return buffer.toByteArray(); }
094
095   /**
096    * Returns the total number of body bytes written so far, including any bytes beyond the capture cap.
097    *
098    * @return The total body length in bytes.
099    */
100   public long getTotalLength() { return totalLength; }
101
102   @Override
103   public ServletOutputStream getOutputStream() throws IOException {
104      if (stream == null)
105         stream = new TeeServletOutputStream(getResponse().getOutputStream());
106      return stream;
107   }
108
109   /**
110    * Returns a character-stream tee that writes through {@link #getOutputStream()}.
111    *
112    * <p>
113    * Juneau's own {@link org.apache.juneau.rest.server.RestResponse#getWriter()} always negotiates its writer over
114    * {@link #getOutputStream()}, so this override chiefly protects direct/raw consumers (filters, non-Juneau
115    * servlets in the same chain) that call {@code HttpServletResponse.getWriter()} on the wrapped response after
116    * capture has been installed &mdash; without it, that call would either bypass the tee entirely or throw
117    * {@link IllegalStateException} because {@link #getOutputStream()} may have already been called.
118    */
119   @Override
120   public PrintWriter getWriter() throws IOException {
121      if (writer == null) {
122         var enc = getCharacterEncoding();
123         var cs = enc == null ? StandardCharsets.ISO_8859_1 : Charset.forName(enc);
124         writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), cs));
125      }
126      return writer;
127   }
128
129   private final class TeeServletOutputStream extends ServletOutputStream {
130
131      private final ServletOutputStream delegate;
132
133      TeeServletOutputStream(ServletOutputStream delegate) {
134         this.delegate = delegate;
135      }
136
137      @Override
138      public void close() throws IOException {
139         delegate.close();
140      }
141
142      @Override
143      public void flush() throws IOException {
144         delegate.flush();
145      }
146
147      @Override
148      public boolean isReady() { return delegate.isReady(); }
149
150      @Override
151      public void setWriteListener(WriteListener writeListener) {
152         delegate.setWriteListener(writeListener);
153      }
154
155      @Override
156      public void write(int b) throws IOException {
157         capture(b);
158         delegate.write(b);
159      }
160
161      @Override
162      public void write(byte[] b, int off, int len) throws IOException {
163         capture(b, off, len);
164         delegate.write(b, off, len);
165      }
166
167      // Only this tee touches the outer response's capture buffer, so these helpers live here rather than on the
168      // outer class.
169      private void capture(int b) {
170         totalLength++;
171         if (buffer.size() < cap)
172            buffer.write(b);
173      }
174
175      private void capture(byte[] b, int off, int len) {
176         totalLength += len;
177         var room = cap - buffer.size();
178         if (room > 0)
179            buffer.write(b, off, Math.min(room, len));
180      }
181   }
182}