1/*
2 * Copyright (C) 2011 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.squareup.okhttp.mockwebserver;
17
18import com.squareup.okhttp.internal.Util;
19import java.io.ByteArrayInputStream;
20import java.io.ByteArrayOutputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.UnsupportedEncodingException;
24import java.util.ArrayList;
25import java.util.Iterator;
26import java.util.List;
27import java.util.concurrent.TimeUnit;
28
29/** A scripted response to be replayed by the mock web server. */
30public final class MockResponse implements Cloneable {
31  private static final String CHUNKED_BODY_HEADER = "Transfer-encoding: chunked";
32
33  private String status = "HTTP/1.1 200 OK";
34  private List<String> headers = new ArrayList<String>();
35
36  /** The response body content, or null if {@code bodyStream} is set. */
37  private byte[] body;
38  /** The response body content, or null if {@code body} is set. */
39  private InputStream bodyStream;
40
41  private int throttleBytesPerPeriod = Integer.MAX_VALUE;
42  private long throttlePeriod = 1;
43  private TimeUnit throttleUnit = TimeUnit.SECONDS;
44
45  private SocketPolicy socketPolicy = SocketPolicy.KEEP_OPEN;
46
47  private int bodyDelayTimeMs = 0;
48
49  private List<PushPromise> promises = new ArrayList<PushPromise>();
50
51  /** Creates a new mock response with an empty body. */
52  public MockResponse() {
53    setBody(new byte[0]);
54  }
55
56  @Override public MockResponse clone() {
57    try {
58      MockResponse result = (MockResponse) super.clone();
59      result.headers = new ArrayList<String>(headers);
60      result.promises = new ArrayList<PushPromise>(promises);
61      return result;
62    } catch (CloneNotSupportedException e) {
63      throw new AssertionError();
64    }
65  }
66
67  /** Returns the HTTP response line, such as "HTTP/1.1 200 OK". */
68  public String getStatus() {
69    return status;
70  }
71
72  public MockResponse setResponseCode(int code) {
73    this.status = "HTTP/1.1 " + code + " OK";
74    return this;
75  }
76
77  public MockResponse setStatus(String status) {
78    this.status = status;
79    return this;
80  }
81
82  /** Returns the HTTP headers, such as "Content-Length: 0". */
83  public List<String> getHeaders() {
84    return headers;
85  }
86
87  /**
88   * Removes all HTTP headers including any "Content-Length" and
89   * "Transfer-encoding" headers that were added by default.
90   */
91  public MockResponse clearHeaders() {
92    headers.clear();
93    return this;
94  }
95
96  /**
97   * Adds {@code header} as an HTTP header. For well-formed HTTP {@code header}
98   * should contain a name followed by a colon and a value.
99   */
100  public MockResponse addHeader(String header) {
101    headers.add(header);
102    return this;
103  }
104
105  /**
106   * Adds a new header with the name and value. This may be used to add multiple
107   * headers with the same name.
108   */
109  public MockResponse addHeader(String name, Object value) {
110    return addHeader(name + ": " + String.valueOf(value));
111  }
112
113  /**
114   * Removes all headers named {@code name}, then adds a new header with the
115   * name and value.
116   */
117  public MockResponse setHeader(String name, Object value) {
118    removeHeader(name);
119    return addHeader(name, value);
120  }
121
122  /** Removes all headers named {@code name}. */
123  public MockResponse removeHeader(String name) {
124    name += ":";
125    for (Iterator<String> i = headers.iterator(); i.hasNext(); ) {
126      String header = i.next();
127      if (name.regionMatches(true, 0, header, 0, name.length())) {
128        i.remove();
129      }
130    }
131    return this;
132  }
133
134  /** Returns the raw HTTP payload, or null if this response is streamed. */
135  public byte[] getBody() {
136    return body;
137  }
138
139  /** Returns an input stream containing the raw HTTP payload. */
140  InputStream getBodyStream() {
141    return bodyStream != null ? bodyStream : new ByteArrayInputStream(body);
142  }
143
144  public MockResponse setBody(byte[] body) {
145    setHeader("Content-Length", body.length);
146    this.body = body;
147    this.bodyStream = null;
148    return this;
149  }
150
151  public MockResponse setBody(InputStream bodyStream, long bodyLength) {
152    setHeader("Content-Length", bodyLength);
153    this.body = null;
154    this.bodyStream = bodyStream;
155    return this;
156  }
157
158  /** Sets the response body to the UTF-8 encoded bytes of {@code body}. */
159  public MockResponse setBody(String body) {
160    try {
161      return setBody(body.getBytes("UTF-8"));
162    } catch (UnsupportedEncodingException e) {
163      throw new AssertionError();
164    }
165  }
166
167  /**
168   * Sets the response body to {@code body}, chunked every {@code maxChunkSize}
169   * bytes.
170   */
171  public MockResponse setChunkedBody(byte[] body, int maxChunkSize) {
172    removeHeader("Content-Length");
173    headers.add(CHUNKED_BODY_HEADER);
174
175    try {
176      ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
177      int pos = 0;
178      while (pos < body.length) {
179        int chunkSize = Math.min(body.length - pos, maxChunkSize);
180        bytesOut.write(Integer.toHexString(chunkSize).getBytes(Util.US_ASCII));
181        bytesOut.write("\r\n".getBytes(Util.US_ASCII));
182        bytesOut.write(body, pos, chunkSize);
183        bytesOut.write("\r\n".getBytes(Util.US_ASCII));
184        pos += chunkSize;
185      }
186      bytesOut.write("0\r\n\r\n".getBytes(Util.US_ASCII)); // Last chunk + empty trailer + crlf.
187
188      this.body = bytesOut.toByteArray();
189      return this;
190    } catch (IOException e) {
191      throw new AssertionError(); // In-memory I/O doesn't throw IOExceptions.
192    }
193  }
194
195  /**
196   * Sets the response body to the UTF-8 encoded bytes of {@code body}, chunked
197   * every {@code maxChunkSize} bytes.
198   */
199  public MockResponse setChunkedBody(String body, int maxChunkSize) {
200    try {
201      return setChunkedBody(body.getBytes("UTF-8"), maxChunkSize);
202    } catch (UnsupportedEncodingException e) {
203      throw new AssertionError();
204    }
205  }
206
207  public SocketPolicy getSocketPolicy() {
208    return socketPolicy;
209  }
210
211  public MockResponse setSocketPolicy(SocketPolicy socketPolicy) {
212    this.socketPolicy = socketPolicy;
213    return this;
214  }
215
216  /**
217   * Throttles the response body writer to sleep for the given period after each
218   * series of {@code bytesPerPeriod} bytes are written. Use this to simulate
219   * network behavior.
220   */
221  public MockResponse throttleBody(int bytesPerPeriod, long period, TimeUnit unit) {
222    this.throttleBytesPerPeriod = bytesPerPeriod;
223    this.throttlePeriod = period;
224    this.throttleUnit = unit;
225    return this;
226  }
227
228  public int getThrottleBytesPerPeriod() {
229    return throttleBytesPerPeriod;
230  }
231
232  public long getThrottlePeriod() {
233    return throttlePeriod;
234  }
235
236  public TimeUnit getThrottleUnit() {
237    return throttleUnit;
238  }
239
240  /**
241   * Set the delayed time of the response body to {@code delay}. This applies to the
242   * response body only; response headers are not affected.
243   */
244  public MockResponse setBodyDelayTimeMs(int delay) {
245    bodyDelayTimeMs = delay;
246    return this;
247  }
248
249  public int getBodyDelayTimeMs() {
250    return bodyDelayTimeMs;
251  }
252
253  /**
254   * When {@link MockWebServer#setNpnProtocols(java.util.List) protocols}
255   * include a SPDY variant, this attaches a pushed stream to this response.
256   */
257  public MockResponse withPush(PushPromise promise) {
258    this.promises.add(promise);
259    return this;
260  }
261
262  /** Returns the streams the server will push with this response. */
263  public List<PushPromise> getPushPromises() {
264    return promises;
265  }
266
267  @Override public String toString() {
268    return status;
269  }
270}
271