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.wicket.protocol.http.mock;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.PrintWriter;
022import java.io.StringWriter;
023import java.text.DateFormat;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Date;
029import java.util.GregorianCalendar;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Locale;
033import java.util.Set;
034import java.util.TimeZone;
035
036import javax.servlet.ServletOutputStream;
037import javax.servlet.WriteListener;
038import javax.servlet.http.Cookie;
039import javax.servlet.http.HttpServletResponse;
040
041import org.apache.wicket.protocol.http.IMetaDataBufferingWebResponse;
042import org.apache.wicket.request.http.WebResponse;
043import org.apache.wicket.util.value.ValueMap;
044
045
046/**
047 * Mock servlet response. Implements all of the methods from the standard HttpServletResponse class
048 * plus helper methods to aid viewing the generated response.
049 * 
050 * @author Chris Turner
051 */
052public class MockHttpServletResponse implements HttpServletResponse, IMetaDataBufferingWebResponse
053{
054        private static final int MODE_BINARY = 1;
055
056        private static final int MODE_NONE = 0;
057
058        private static final int MODE_TEXT = 2;
059
060        private ByteArrayOutputStream byteStream;
061
062        private String characterEncoding = "UTF-8";
063
064        private final List<Cookie> cookies = new ArrayList<Cookie>();
065
066        private String errorMessage = null;
067
068        private final ValueMap headers = new ValueMap();
069
070        private Locale locale = null;
071
072        private int mode = MODE_NONE;
073
074        private PrintWriter printWriter;
075
076        private String redirectLocation = null;
077
078        private ServletOutputStream servletStream;
079
080        private int status = HttpServletResponse.SC_OK;
081
082        private StringWriter stringWriter;
083
084        /**
085         * Create the response object.
086         * 
087         * @param servletRequest
088         */
089        public MockHttpServletResponse(MockHttpServletRequest servletRequest)
090        {
091                initialize();
092        }
093
094        /**
095         * Add a cookie to the response.
096         * 
097         * @param cookie
098         *            The cookie to add
099         */
100        @Override
101        public void addCookie(final Cookie cookie)
102        {
103                // remove any potential duplicates
104                // see http://www.ietf.org/rfc/rfc2109.txt, p.4.3.3
105                Iterator<Cookie> iterator = cookies.iterator();
106                while (iterator.hasNext())
107                {
108                        Cookie old = iterator.next();
109                        if (Cookies.isEqual(cookie, old))
110                        {
111                                iterator.remove();
112                        }
113                }
114                cookies.add(cookie);
115        }
116
117        /**
118         * Add a date header.
119         * 
120         * @param name
121         *            The header value
122         * @param l
123         *            The long value
124         */
125        @Override
126        public void addDateHeader(String name, long l)
127        {
128                DateFormat df = DateFormat.getDateInstance(DateFormat.FULL);
129                addHeader(name, df.format(new Date(l)));
130        }
131
132        /**
133         * Add the given header value, including an additional entry if one already exists.
134         * 
135         * @param name
136         *            The name for the header
137         * @param value
138         *            The value for the header
139         */
140        @Override
141        @SuppressWarnings("unchecked")
142        public void addHeader(final String name, final String value)
143        {
144                List<String> list = (List<String>)headers.get(name);
145                if (list == null)
146                {
147                        list = new ArrayList<String>(1);
148                        headers.put(name, list);
149                }
150                list.add(value);
151        }
152
153        /**
154         * Add an int header value.
155         * 
156         * @param name
157         *            The header name
158         * @param i
159         *            The value
160         */
161        @Override
162        public void addIntHeader(final String name, final int i)
163        {
164                addHeader(name, "" + i);
165        }
166
167        /**
168         * Check if the response contains the given header name.
169         * 
170         * @param name
171         *            The name to check
172         * @return Whether header in response or not
173         */
174        @Override
175        public boolean containsHeader(final String name)
176        {
177                return headers.containsKey(name);
178        }
179
180        /**
181         * Encode the redirectLocation URL. Does no changes as this test implementation uses cookie
182         * based url tracking.
183         * 
184         * @param url
185         *            The url to encode
186         * @return The encoded url
187         */
188        @Override
189        public String encodeRedirectUrl(final String url)
190        {
191                return url;
192        }
193
194        /**
195         * Encode the redirectLocation URL. Does no changes as this test implementation uses cookie
196         * based url tracking.
197         * 
198         * @param url
199         *            The url to encode
200         * @return The encoded url
201         */
202        @Override
203        public String encodeRedirectURL(final String url)
204        {
205                return url;
206        }
207
208        /**
209         * Encode the URL. Does no changes as this test implementation uses cookie based url tracking.
210         * 
211         * @param url
212         *            The url to encode
213         * @return The encoded url
214         */
215        @Override
216        public String encodeUrl(final String url)
217        {
218                return url;
219        }
220
221        /**
222         * Encode the URL. Does no changes as this test implementation uses cookie based url tracking.
223         * 
224         * @param url
225         *            The url to encode
226         * @return The encoded url
227         */
228        @Override
229        public String encodeURL(final String url)
230        {
231                return url;
232        }
233
234        /**
235         * Flush the buffer.
236         * 
237         * @throws IOException
238         */
239        @Override
240        public void flushBuffer() throws IOException
241        {
242        }
243
244        /**
245         * Get the binary content that was written to the servlet stream.
246         * 
247         * @return The binary content
248         */
249        public byte[] getBinaryContent()
250        {
251                return byteStream.toByteArray();
252        }
253
254        /**
255         * Return the current buffer size
256         * 
257         * @return The buffer size
258         */
259        @Override
260        public int getBufferSize()
261        {
262                if (mode == MODE_NONE)
263                {
264                        return 0;
265                }
266                else if (mode == MODE_BINARY)
267                {
268                        return byteStream.size();
269                }
270                else
271                {
272                        return stringWriter.getBuffer().length();
273                }
274        }
275
276        /**
277         * Get the character encoding of the response.
278         * 
279         * @return The character encoding
280         */
281        @Override
282        public String getCharacterEncoding()
283        {
284                return characterEncoding;
285        }
286
287
288        /**
289         * Get all of the cookies that have been added to the response.
290         * 
291         * @return The collection of cookies
292         */
293        public List<Cookie> getCookies()
294        {
295                List<Cookie> copies = new ArrayList<Cookie>();
296                for (Cookie cookie : cookies)
297                {
298                        copies.add(Cookies.copyOf(cookie));
299                }
300                return copies;
301        }
302
303        /**
304         * Get the text document that was written as part of this response.
305         * 
306         * @return The document
307         */
308        public String getDocument()
309        {
310                if (mode == MODE_BINARY)
311                {
312                        return new String(byteStream.toByteArray());
313                }
314                else
315                {
316                        return stringWriter.getBuffer().toString();
317                }
318        }
319
320        /**
321         * Get the error message.
322         * 
323         * @return The error message, or null if no message
324         */
325        public String getErrorMessage()
326        {
327                return errorMessage;
328        }
329
330        /**
331         * Return the value of the given named header.
332         * 
333         * @param name
334         *            The header name
335         * @return The value, or null
336         */
337        @Override
338        @SuppressWarnings("unchecked")
339        public String getHeader(final String name)
340        {
341                List<String> l = (List<String>)headers.get(name);
342                if (l == null || l.size() < 1)
343                {
344                        return null;
345                }
346                else
347                {
348                        return l.get(0);
349                }
350        }
351
352        /**
353         * Get the names of all of the headers.
354         * 
355         * @return The header names
356         */
357        @Override
358        public Set<String> getHeaderNames()
359        {
360                return headers.keySet();
361        }
362
363        /**
364         * Get the encoded locale
365         * 
366         * @return The locale
367         */
368        @Override
369        public Locale getLocale()
370        {
371                return locale;
372        }
373
374        /**
375         * Get the output stream for writing binary data from the servlet.
376         * 
377         * @return The binary output stream.
378         */
379        @Override
380        public ServletOutputStream getOutputStream()
381        {
382                if (mode == MODE_TEXT)
383                {
384                        throw new IllegalArgumentException("Can't write binary after already selecting text");
385                }
386                mode = MODE_BINARY;
387                return servletStream;
388        }
389
390        /**
391         * Get the location that was redirected to.
392         * 
393         * @return The redirect location, or null if not a redirect
394         */
395        public String getRedirectLocation()
396        {
397                return redirectLocation;
398        }
399
400        /**
401         * Get the status code.
402         * 
403         * @return The status code
404         */
405        @Override
406        public int getStatus()
407        {
408                return status;
409        }
410
411        /**
412         * Get the print writer for writing text output for this response.
413         * 
414         * @return The writer
415         * @throws IOException
416         *             Not used
417         */
418        @Override
419        public PrintWriter getWriter() throws IOException
420        {
421                if (mode == MODE_BINARY)
422                {
423                        throw new IllegalArgumentException("Can't write text after already selecting binary");
424                }
425                mode = MODE_TEXT;
426                return printWriter;
427        }
428
429        /**
430         * Reset the response ready for reuse.
431         */
432        public void initialize()
433        {
434                cookies.clear();
435                headers.clear();
436                errorMessage = null;
437                redirectLocation = null;
438                status = HttpServletResponse.SC_OK;
439                characterEncoding = "UTF-8";
440                locale = null;
441
442                byteStream = new ByteArrayOutputStream();
443                servletStream = new ServletOutputStream()
444                {
445                        @Override
446                        public boolean isReady()
447                        {
448                                return true;
449                        }
450
451                        @Override
452                        public void setWriteListener(WriteListener writeListener)
453                        {
454                        }
455
456                        @Override
457                        public void write(int b)
458                        {
459                                byteStream.write(b);
460                        }
461                };
462                stringWriter = new StringWriter();
463                printWriter = new PrintWriter(stringWriter)
464                {
465                        @Override
466                        public void close()
467                        {
468                                // Do nothing
469                        }
470
471                        @Override
472                        public void flush()
473                        {
474                                // Do nothing
475                        }
476                };
477                mode = MODE_NONE;
478        }
479
480        /**
481         * Always returns false.
482         * 
483         * @return Always false
484         */
485        @Override
486        public boolean isCommitted()
487        {
488                return false;
489        }
490
491        /**
492         * Return whether the servlet returned an error code or not.
493         * 
494         * @return Whether an error occurred or not
495         */
496        public boolean isError()
497        {
498                return (status != HttpServletResponse.SC_OK);
499        }
500
501        /**
502         * Check whether the response was redirected or not.
503         * 
504         * @return Whether the state was redirected or not
505         */
506        public boolean isRedirect()
507        {
508                return (redirectLocation != null);
509        }
510
511        /**
512         * Delegate to initialize method.
513         */
514        @Override
515        public void reset()
516        {
517                initialize();
518        }
519
520        /**
521         * Clears the buffer.
522         */
523        @Override
524        public void resetBuffer()
525        {
526                if (mode == MODE_BINARY)
527                {
528                        byteStream.reset();
529                }
530                else if (mode == MODE_TEXT)
531                {
532                        stringWriter.getBuffer().delete(0, stringWriter.getBuffer().length());
533                }
534        }
535
536        /**
537         * Send an error code. This implementation just sets the internal error state information.
538         * 
539         * @param code
540         *            The code
541         * @throws IOException
542         *             Not used
543         */
544        @Override
545        public void sendError(final int code) throws IOException
546        {
547                status = code;
548                errorMessage = null;
549        }
550
551        /**
552         * Send an error code. This implementation just sets the internal error state information.
553         * 
554         * @param code
555         *            The error code
556         * @param msg
557         *            The error message
558         * @throws IOException
559         *             Not used
560         */
561        @Override
562        public void sendError(final int code, final String msg) throws IOException
563        {
564                status = code;
565                errorMessage = msg;
566        }
567
568        /**
569         * Indicate sending of a redirectLocation to a particular named resource. This implementation
570         * just keeps hold of the redirectLocation info and makes it available for query.
571         * 
572         * @param location
573         *            The location to redirectLocation to
574         * @throws IOException
575         *             Not used
576         */
577        @Override
578        public void sendRedirect(String location) throws IOException
579        {
580                redirectLocation = location;
581                status = HttpServletResponse.SC_FOUND;
582        }
583
584        /**
585         * Method ignored.
586         * 
587         * @param size
588         *            The size
589         */
590        @Override
591        public void setBufferSize(final int size)
592        {
593        }
594
595        /**
596         * Set the character encoding.
597         * 
598         * @param characterEncoding
599         *            The character encoding
600         */
601        @Override
602        public void setCharacterEncoding(final String characterEncoding)
603        {
604                this.characterEncoding = characterEncoding;
605        }
606
607        /**
608         * Set the content length.
609         * 
610         * @param length
611         *            The length
612         */
613        @Override
614        public void setContentLength(final int length)
615        {
616                setIntHeader("Content-Length", length);
617        }
618
619        @Override
620        public void setContentLengthLong(long len)
621        {
622                setContentLength((int) len);
623        }
624
625        /**
626         * Set the content type.
627         * 
628         * @param type
629         *            The content type
630         */
631        @Override
632        public void setContentType(final String type)
633        {
634                setHeader("Content-Type", type);
635        }
636
637        /**
638         * @return value of content-type header
639         */
640        @Override
641        public String getContentType()
642        {
643                return getHeader("Content-Type");
644        }
645
646        /**
647         * Set a date header.
648         * 
649         * @param name
650         *            The header name
651         * @param l
652         *            The long value
653         */
654        @Override
655        public void setDateHeader(final String name, final long l)
656        {
657                setHeader(name, formatDate(l));
658        }
659
660        /**
661         * @param l
662         * @return formatted date
663         */
664        public static String formatDate(long l)
665        {
666                StringBuilder _dateBuffer = new StringBuilder(32);
667                Calendar _calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
668                _calendar.setTimeInMillis(l);
669                formatDate(_dateBuffer, _calendar, false);
670                return _dateBuffer.toString();
671        }
672
673        /* BEGIN: This code comes from Jetty 6.1.1 */
674        private static String[] DAYS = { "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
675        private static String[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
676                        "Sep", "Oct", "Nov", "Dec", "Jan" };
677
678        /**
679         * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'" or "EEE, dd-MMM-yy HH:mm:ss 'GMT'"for
680         * cookies
681         * 
682         * @param buf
683         * @param calendar
684         * @param cookie
685         */
686        public static void formatDate(StringBuilder buf, Calendar calendar, boolean cookie)
687        {
688                // "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
689                // "EEE, dd-MMM-yy HH:mm:ss 'GMT'", cookie
690
691                int day_of_week = calendar.get(Calendar.DAY_OF_WEEK);
692                int day_of_month = calendar.get(Calendar.DAY_OF_MONTH);
693                int month = calendar.get(Calendar.MONTH);
694                int year = calendar.get(Calendar.YEAR);
695                int century = year / 100;
696                year = year % 100;
697
698                int epoch = (int)((calendar.getTimeInMillis() / 1000) % (60 * 60 * 24));
699                int seconds = epoch % 60;
700                epoch = epoch / 60;
701                int minutes = epoch % 60;
702                int hours = epoch / 60;
703
704                buf.append(DAYS[day_of_week]);
705                buf.append(',');
706                buf.append(' ');
707                append2digits(buf, day_of_month);
708
709                if (cookie)
710                {
711                        buf.append('-');
712                        buf.append(MONTHS[month]);
713                        buf.append('-');
714                        append2digits(buf, year);
715                }
716                else
717                {
718                        buf.append(' ');
719                        buf.append(MONTHS[month]);
720                        buf.append(' ');
721                        append2digits(buf, century);
722                        append2digits(buf, year);
723                }
724                buf.append(' ');
725                append2digits(buf, hours);
726                buf.append(':');
727                append2digits(buf, minutes);
728                buf.append(':');
729                append2digits(buf, seconds);
730                buf.append(" GMT");
731        }
732
733        /**
734         * @param buf
735         * @param i
736         */
737        public static void append2digits(StringBuilder buf, int i)
738        {
739                if (i < 100)
740                {
741                        buf.append((char)(i / 10 + '0'));
742                        buf.append((char)(i % 10 + '0'));
743                }
744        }
745
746        /* END: This code comes from Jetty 6.1.1 */
747
748        /**
749         * Set the given header value.
750         * 
751         * @param name
752         *            The name for the header
753         * @param value
754         *            The value for the header
755         */
756        @Override
757        public void setHeader(final String name, final String value)
758        {
759                List<String> l = new ArrayList<String>(1);
760                l.add(value);
761                headers.put(name, l);
762        }
763
764        /**
765         * Set an int header value.
766         * 
767         * @param name
768         *            The header name
769         * @param i
770         *            The value
771         */
772        @Override
773        public void setIntHeader(final String name, final int i)
774        {
775                setHeader(name, "" + i);
776        }
777
778        /**
779         * Set the locale in the response header.
780         * 
781         * @param locale
782         *            The locale
783         */
784        @Override
785        public void setLocale(final Locale locale)
786        {
787                this.locale = locale;
788        }
789
790        /**
791         * Set the status for this response.
792         * 
793         * @param status
794         *            The status
795         */
796        @Override
797        public void setStatus(final int status)
798        {
799                this.status = status;
800        }
801
802        /**
803         * Set the status for this response.
804         * 
805         * @param status
806         *            The status
807         * @param msg
808         *            The message
809         * @deprecated
810         */
811        @Override
812        @Deprecated
813        public void setStatus(final int status, final String msg)
814        {
815                setStatus(status);
816        }
817
818        /**
819         * @return binary response
820         */
821        public String getBinaryResponse()
822        {
823                String ctheader = getHeader("Content-Length");
824                if (ctheader == null)
825                {
826                        return getDocument();
827                }
828                else
829                {
830                        return getDocument().substring(0, Integer.valueOf(ctheader));
831                }
832        }
833
834        /**
835         * @param name
836         * @return headers with given name
837         */
838        @Override
839        public Collection<String> getHeaders(String name)
840        {
841                return Collections.singletonList(headers.get(name).toString());
842        }
843
844        @Override
845        public void writeMetaData(WebResponse webResponse)
846        {
847                for (Cookie cookie : cookies)
848                {
849                        webResponse.addCookie(cookie);
850                }
851                for (String name : headers.keySet())
852                {
853                        webResponse.setHeader(name, headers.get(name).toString());
854                }
855                webResponse.setStatus(status);
856        }
857}