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