001 package hirondelle.fish.test.doubles;
002
003 import java.io.*;
004 import java.util.*;
005 import java.text.SimpleDateFormat;
006 import hirondelle.web4j.util.Util;
007 import hirondelle.web4j.util.Args;
008 import hirondelle.web4j.model.ModelUtil;
009 import javax.servlet.ServletOutputStream;
010 import javax.servlet.http.Cookie;
011 import javax.servlet.http.HttpServletResponse;
012
013 /**
014 Fake implementation of {@link HttpServletResponse}, used
015 only for testing outside of the regular runtime environment.
016
017 Internally, a fake {@link ServletOutputStream} is used here, which simply
018 places the response data in a simple byte array held in memory, with no
019 other ultimate destination such as a file or another stream.
020 Thus, <tt>flush</tt> and <tt>close</tt> are no-operations, and
021 there is no reason to use buffering with such a stream.
022
023 <P>Methods associated with buffering are no-operations.
024 */
025 public final class FakeResponse implements HttpServletResponse {
026
027 /*
028 Methods added for testing.
029 */
030
031 /**
032 Return the response as a <tt>String</tt>.
033 Method added for testing.
034 */
035 public String getFinalResponse(String aEncoding){
036 return fStream.getString(aEncoding);
037 }
038
039 /**
040 Return the response as a <tt>byte[]</tt>.
041 Method added for testing.
042 */
043 public byte[] getFinalResponseAsBytes(){
044 return fStream.getBytes();
045 }
046
047 /**
048 Return the cookies that have been passed to this object.
049 Method added for testing.
050 */
051 public List<Cookie> getCookies(){ return fCookies; }
052
053 /**
054 Return the response status code.
055 Method added for testing.
056 */
057 public int getStatus() { return fStatusCode; }
058
059 /**
060 Return all headers associated with the response.
061 Method added for testing.
062 */
063 public List<Header> getHeaders() { return fHeaders; }
064
065 /*
066 ServletResponse methods.
067 */
068
069 public String getCharacterEncoding() { return fCharacterEncoding; }
070 public void setCharacterEncoding(String aEncoding) {
071 if( ! fIsCommitted && ! fHasCalledWriter ) {
072 Args.checkForContent(aEncoding);
073 fCharacterEncoding = aEncoding;
074 }
075 }
076
077 public String getContentType() { return fContentType; }
078 public void setContentType(String aContentType) {
079 if( ! fIsCommitted ) {
080 Args.checkForContent(aContentType);
081 StringTokenizer parser = new StringTokenizer(aContentType, ";");
082 String contentType = parser.nextToken();
083 String charEncoding = parser.nextToken().trim().substring("charset=".length());
084 fContentType = aContentType; //always the whole thing
085 if( ! fHasCalledWriter && Util.textHasContent(charEncoding) ) {
086 setCharacterEncoding(charEncoding);
087 }
088 }
089 }
090
091 public Locale getLocale() { return fLocale; }
092
093 /** Does not set the character encoding. */
094 public void setLocale(Locale aLocale) {
095 if( ! fIsCommitted ) {
096 fLocale = aLocale;
097 }
098 }
099
100 public ServletOutputStream getOutputStream() throws IOException {
101 if(fHasCalledWriter) throw new IllegalStateException("Cannot use both Stream and Writer.");
102 fHasCalledStream = true;
103 fStream = new FakeServletOutputStream(this);
104 return fStream;
105 }
106
107 public PrintWriter getWriter() throws IOException {
108 if(fHasCalledStream) throw new IllegalStateException("Cannot use both Stream and Writer.");
109 fHasCalledWriter = true;
110 fStream = new FakeServletOutputStream(this);
111 fWriter = new PrintWriter(new OutputStreamWriter(fStream, fCharacterEncoding));
112 return fWriter;
113 }
114
115 /** No-operation. */
116 public void setContentLength(int aLength) { }
117
118 public int getBufferSize() { return fBufferSize; }
119
120 public void setBufferSize(int aBufferSize) { fBufferSize = aBufferSize; }
121
122 /** No-operation. */
123 public void flushBuffer() throws IOException { }
124
125 /** No-operation. */
126 public void resetBuffer() { }
127
128 /** Returns <tt>true</tt> is anything has been written to the in-memory stream. */
129 public boolean isCommitted() { return fIsCommitted; }
130
131 /** No-operation. */
132 public void reset() { }
133
134 /*
135 HttpServletResponse methods.
136 */
137
138 public void addCookie(Cookie aCookie) {
139 fCookies.add(aCookie);
140 }
141
142 public void setStatus(int aStatusCode) {
143 fStatusCode = aStatusCode;
144 }
145
146 /** Not implemented - deprecated. */
147 public void setStatus(int aStatusCode, String aStatusMessage) { }
148
149 public boolean containsHeader(String aName) {
150 boolean result = false;
151 Args.checkForContent(aName);
152 for(Header header : fHeaders){
153 if ( aName.equals(header.getName()) ){
154 result = true;
155 break;
156 }
157 }
158 return result;
159 }
160
161 public void addHeader(String aName, String aValue) {
162 fHeaders.add(new Header(aName, aValue));
163 }
164
165 public void setHeader(String aName, String aValue) {
166 Args.checkForContent(aName);
167 Args.checkForContent(aValue);
168 if( containsHeader(aName) ){
169 replaceExistingHeader(aName, aValue);
170 }
171 else {
172 addHeader(aName, aValue);
173 }
174 }
175
176 public void addIntHeader(String aName, int aValue) {
177 addHeader(aName, new Integer(aValue).toString());
178 }
179
180 public void setIntHeader(String aName, int aValue) {
181 setHeader(aName, new Integer(aValue).toString());
182 }
183
184 public void addDateHeader(String aName, long aValue) {
185 String date = new SimpleDateFormat(PATTERN_RFC1123).format(new Date(aValue));
186 addHeader(aName, date);
187 }
188
189 public void setDateHeader(String aName, long aValue) {
190 String date = new SimpleDateFormat(PATTERN_RFC1123).format(new Date(aValue));
191 setHeader(aName, date);
192 }
193
194 /** Returns the argument unchanged. */
195 public String encodeURL(String aURL) { return aURL; }
196
197 /** Returns the argument unchanged. */
198 public String encodeRedirectURL(String aURL) { return aURL; }
199
200 /** Deprecated. Returns <tt>null</tt>. */
201 public String encodeUrl(String arg0) { return null; }
202
203 /** Deprecated. Returns <tt>null</tt>. */
204 public String encodeRedirectUrl(String arg0) { return null; }
205
206 public void sendError(int aStatusCode) throws IOException {
207 if(fIsCommitted) throw new IllegalStateException("Cannot set status after response is committed.");
208 fStatusCode = aStatusCode;
209 }
210
211 public void sendError(int aStatusCode, String aMessage) throws IOException {
212 if(fIsCommitted) throw new IllegalStateException("Cannot set status after response is committed.");
213 fStatusCode = aStatusCode;
214 }
215
216 public void sendRedirect(String aLocation) throws IOException {
217 if(fIsCommitted) throw new IllegalStateException("Cannot send redirect after response is committed.");
218 }
219
220 /** Holds simple name-value pairs. */
221 public static final class Header{
222 Header(String aName, String aValue){
223 Args.checkForContent(aName);
224 Args.checkForContent(aValue);
225 fName = aName;
226 fValue = aValue;
227 }
228 public String getName() { return fName; }
229 public String getValue() { return fValue; }
230 @Override public String toString() {
231 return ModelUtil.toStringFor(this);
232 }
233 private final String fName;
234 private final String fValue;
235 }
236
237 // PRIVATE //
238 private String fCharacterEncoding = "ISO-8859-1";
239 private String fContentType;
240 private Locale fLocale = Locale.ENGLISH;
241
242 private FakeServletOutputStream fStream;
243 private PrintWriter fWriter;
244 private boolean fHasCalledStream;
245 private boolean fHasCalledWriter;
246 private boolean fIsCommitted;
247 private int fBufferSize;
248
249 private List<Cookie> fCookies = new ArrayList<Cookie>();
250 private int fStatusCode;
251 private List<Header> fHeaders = new ArrayList<Header>();
252 private static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
253
254 /**
255 Simply writes bytes into memory. No buffering required.
256 Flush and close are no-operations.
257 */
258 private static final class FakeServletOutputStream extends ServletOutputStream{
259 FakeServletOutputStream(FakeResponse aFakeResponse){
260 //(will expand as required)
261 fOutput = new ByteArrayOutputStream(1024);
262 fFakeResponse = aFakeResponse;
263 }
264 @Override public void write(int aByte) throws IOException {
265 fOutput.write(aByte);
266 fFakeResponse.fIsCommitted = true;
267 }
268 byte[] getBytes(){
269 return fOutput.toByteArray();
270 }
271 String getString(String aEncoding){
272 String result = null;
273 try {
274 result = fOutput.toString(aEncoding);
275 }
276 catch (UnsupportedEncodingException ex){
277 throw new IllegalArgumentException("Unsupported encoding: " + Util.quote(aEncoding));
278 }
279 return result;
280 }
281 /** Not a buffer, but rather the actual destination for the data. */
282 private ByteArrayOutputStream fOutput;
283 private FakeResponse fFakeResponse;
284 }
285
286 private void replaceExistingHeader(String aName, String aValue){
287 if( ! containsHeader(aName) ) {
288 throw new IllegalArgumentException("Cannot replace header, since does not exist.");
289 }
290 //remove the old
291 Iterator<Header> iter = fHeaders.iterator();
292 while ( iter.hasNext() ) {
293 Header header = iter.next();
294 if( header.getName().equals(aName)) {
295 iter.remove();
296 }
297 }
298 //add the new
299 addHeader(aName, aValue);
300 }
301 }