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