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 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 — 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 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 — 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}