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 }