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    }