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.mock;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.time.*;
024import java.time.format.*;
025import java.util.*;
026
027import org.apache.juneau.rest.server.util.*;
028
029import jakarta.servlet.*;
030import jakarta.servlet.http.*;
031
032/**
033 * An implementation of {@link HttpServletResponse} for mocking purposes.
034 *
035 * <h5 class='section'>See Also:</h5><ul>
036 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestMock">juneau-rest-mock</a>
037 * </ul>
038*/
039@SuppressWarnings({
040   "java:S115", // Constants use UPPER_snakeCase convention (e.g., HEADER_ContentType)
041   "java:S4144", // Identical methods intentional for different test scenarios
042   "resource" // Streams returned to servlet container; lifecycle managed by the container
043})
044public class MockServletResponse implements HttpServletResponse {
045
046   private static final String HEADER_ContentType = "Content-Type";
047
048   /**
049    * Creates a new servlet response.
050    *
051    * @return A new response.
052    */
053   public static MockServletResponse create() {
054      return new MockServletResponse();
055   }
056
057   private String characterEncoding = "UTF-8";
058   private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
059   private int bufferSize;
060   private Locale locale;
061   private int sc = 200;
062   private String msg;
063
064   private final Map<String,String[]> headerMap = map();
065
066   @Override /* Overridden from HttpServletResponse */
067   public void addCookie(Cookie cookie) {
068      // No-op: Mock implementation - full functionality not required
069   }
070
071   @Override /* Overridden from HttpServletResponse */
072   public void addDateHeader(String name, long date) {
073      Instant instant = Instant.ofEpochMilli(date);
074      DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME
075         .withZone(ZoneId.of("GMT"));
076      headerMap.put(name, a(formatter.format(instant)));
077   }
078
079   @Override /* Overridden from HttpServletResponse */
080   public void addHeader(String name, String value) {
081      headerMap.put(name, a(value));
082   }
083
084   @Override /* Overridden from HttpServletResponse */
085   public void addIntHeader(String name, int value) {
086      headerMap.put(name, a(String.valueOf(value)));
087   }
088
089   @Override /* Overridden from HttpServletResponse */
090   public boolean containsHeader(String name) {
091      return nn(getHeader(name));
092   }
093
094   @Override /* Overridden from HttpServletResponse */
095   public String encodeRedirectURL(String url) {
096      return null;
097   }
098
099   @Override /* Overridden from HttpServletResponse */
100   public String encodeURL(String url) {
101      return null;
102   }
103
104   @Override /* Overridden from HttpServletResponse */
105   public void flushBuffer() throws IOException {
106      // No-op: Mock implementation - full functionality not required
107   }
108
109   @Override /* Overridden from HttpServletResponse */
110   public int getBufferSize() { return bufferSize; }
111
112   @Override /* Overridden from HttpServletResponse */
113   public String getCharacterEncoding() { return characterEncoding; }
114
115   @Override /* Overridden from HttpServletResponse */
116   public String getContentType() { return getHeader(HEADER_ContentType); }
117
118   @Override /* Overridden from HttpServletResponse */
119   public String getHeader(String name) {
120      String[] s = headerMap.get(name);
121      return s == null || s.length == 0 ? null : s[0];
122   }
123
124   @Override /* Overridden from HttpServletResponse */
125   public Collection<String> getHeaderNames() { return headerMap.keySet(); }
126
127   @Override /* Overridden from HttpServletResponse */
128   public Collection<String> getHeaders(String name) {
129      String[] s = headerMap.get(name);
130      return s == null ? Collections.emptyList() : u(l(s));
131   }
132
133   @Override /* Overridden from HttpServletResponse */
134   public Locale getLocale() { return locale; }
135
136   /**
137    * Returns the response message.
138    *
139    * @return The response message.
140    */
141   public String getMessage() { return msg; }
142
143   @Override /* Overridden from HttpServletResponse */
144   public ServletOutputStream getOutputStream() throws IOException { return new FinishableServletOutputStream(baos); }
145
146   @Override /* Overridden from HttpServletResponse */
147   public int getStatus() { return sc; }
148
149   @Override /* Overridden from HttpServletResponse */
150   public PrintWriter getWriter() throws IOException { return new PrintWriter(new OutputStreamWriter(getOutputStream(), characterEncoding)); }
151
152   /**
153    * Fluent setter for {@link #setHeader(String,String)}.
154    *
155    * @param name The header name.
156    * @param value The new header value.
157    * @return This object.
158    */
159   public MockServletResponse header(String name, String value) {
160      setHeader(name, value);
161      return this;
162   }
163
164   @Override /* Overridden from HttpServletResponse */
165   public boolean isCommitted() { return false; }
166
167   @Override /* Overridden from HttpServletResponse */
168   public void reset() {
169      // No-op: Mock implementation - full functionality not required
170   }
171
172   @Override /* Overridden from HttpServletResponse */
173   public void resetBuffer() {
174      // Mock implementation - no-op for testing purposes
175   }
176
177   @Override /* Overridden from HttpServletResponse */
178   public void sendError(int sc) throws IOException {
179      this.sc = sc;
180   }
181
182   @Override /* Overridden from HttpServletResponse */
183   public void sendError(int sc, String msg) throws IOException {
184      this.sc = sc;
185      this.msg = msg;
186   }
187
188   @Override /* Overridden from HttpServletResponse */
189   public void sendRedirect(String location) throws IOException {
190      this.sc = 302;
191      headerMap.put("Location", a(location));
192   }
193
194   @Override /* Overridden from HttpServletResponse */
195   public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException {
196      this.sc = sc;
197      headerMap.put("Location", a(location));
198   }
199
200   @Override /* Overridden from HttpServletResponse */
201   public void setBufferSize(int size) { this.bufferSize = size; }
202
203   @Override /* Overridden from HttpServletResponse */
204   public void setCharacterEncoding(String charset) {
205      this.characterEncoding = charset;
206      updateContentTypeHeader();
207   }
208
209   @Override /* Overridden from HttpServletResponse */
210   public void setContentLength(int len) {
211      header("Content-Length", String.valueOf(len));
212   }
213
214   @Override /* Overridden from HttpServletResponse */
215   public void setContentLengthLong(long len) {
216      header("Content-Length", String.valueOf(len));
217   }
218
219   @Override /* Overridden from HttpServletResponse */
220   public void setContentType(String type) {
221      setHeader(HEADER_ContentType, type);
222      updateContentTypeHeader();
223   }
224
225   @Override /* Overridden from HttpServletResponse */
226   public void setDateHeader(String name, long date) {
227      Instant instant = Instant.ofEpochMilli(date);
228      DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME
229         .withZone(ZoneId.of("GMT"));
230      headerMap.put(name, a(formatter.format(instant)));
231   }
232
233   @Override /* Overridden from HttpServletResponse */
234   public void setHeader(String name, String value) {
235      headerMap.put(name, a(value));
236   }
237
238   @Override /* Overridden from HttpServletResponse */
239   public void setIntHeader(String name, int value) {
240      headerMap.put(name, a(String.valueOf(value)));
241   }
242
243   @Override /* Overridden from HttpServletResponse */
244   public void setLocale(Locale loc) { this.locale = loc; }
245
246   @Override /* Overridden from HttpServletResponse */
247   public void setStatus(int sc) { this.sc = sc; }
248
249   /**
250    * Fluent setter for {@link #setStatus(int)}.
251    *
252    * @param value The new property value.
253    * @return This object.
254    */
255   public MockServletResponse status(int value) {
256      setStatus(value);
257      return this;
258   }
259
260   private void updateContentTypeHeader() {
261      var contentType = getContentType();
262      String charset = characterEncoding;
263      if (nn(contentType) && nn(charset)) {
264         if (contentType.indexOf("charset=") != -1)
265            contentType = contentType.replaceAll("\\;\\s*charset=.*", "");
266         if (neqic("UTF-8", charset))
267            contentType = contentType + ";charset=" + charset;
268         header(HEADER_ContentType, contentType);
269      }
270   }
271
272   /**
273    * Returns the raw bytes written to the response body.
274    *
275    * @return The response body bytes. Never <jk>null</jk>.
276    * @throws IOException If the stream cannot be flushed.
277    */
278   public byte[] getContent() throws IOException {
279      baos.flush();
280      return baos.toByteArray();
281   }
282
283   /**
284    * Returns the raw header map backing this response.
285    *
286    * @return The header map, keyed by header name to all values set for that name. Never <jk>null</jk>.
287    */
288   public Map<String,String[]> getHeaders() { return headerMap; }
289}