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 jakarta.servlet.ServletOutputStream;
037import jakarta.servlet.WriteListener;
038import jakarta.servlet.http.Cookie;
039import jakarta.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 URL. Does no changes as this test implementation uses cookie based url tracking.
196         * 
197         * @param url
198         *            The url to encode
199         * @return The encoded url
200         */
201        @Override
202        public String encodeURL(final String url)
203        {
204                return url;
205        }
206
207        /**
208         * Flush the buffer.
209         * 
210         * @throws IOException
211         */
212        @Override
213        public void flushBuffer() throws IOException
214        {
215        }
216
217        /**
218         * Get the binary content that was written to the servlet stream.
219         * 
220         * @return The binary content
221         */
222        public byte[] getBinaryContent()
223        {
224                return byteStream.toByteArray();
225        }
226
227        /**
228         * Return the current buffer size
229         * 
230         * @return The buffer size
231         */
232        @Override
233        public int getBufferSize()
234        {
235                if (mode == MODE_NONE)
236                {
237                        return 0;
238                }
239                else if (mode == MODE_BINARY)
240                {
241                        return byteStream.size();
242                }
243                else
244                {
245                        return stringWriter.getBuffer().length();
246                }
247        }
248
249        /**
250         * Get the character encoding of the response.
251         * 
252         * @return The character encoding
253         */
254        @Override
255        public String getCharacterEncoding()
256        {
257                return characterEncoding;
258        }
259
260
261        /**
262         * Get all of the cookies that have been added to the response.
263         * 
264         * @return The collection of cookies
265         */
266        public List<Cookie> getCookies()
267        {
268                List<Cookie> copies = new ArrayList<Cookie>();
269                for (Cookie cookie : cookies)
270                {
271                        copies.add(Cookies.copyOf(cookie));
272                }
273                return copies;
274        }
275
276        /**
277         * Get the text document that was written as part of this response.
278         * 
279         * @return The document
280         */
281        public String getDocument()
282        {
283                if (mode == MODE_BINARY)
284                {
285                        return new String(byteStream.toByteArray());
286                }
287                else
288                {
289                        return stringWriter.getBuffer().toString();
290                }
291        }
292
293        /**
294         * Get the error message.
295         * 
296         * @return The error message, or null if no message
297         */
298        public String getErrorMessage()
299        {
300                return errorMessage;
301        }
302
303        /**
304         * Return the value of the given named header.
305         * 
306         * @param name
307         *            The header name
308         * @return The value, or null
309         */
310        @Override
311        @SuppressWarnings("unchecked")
312        public String getHeader(final String name)
313        {
314                List<String> l = (List<String>)headers.get(name);
315                if (l == null || l.size() < 1)
316                {
317                        return null;
318                }
319                else
320                {
321                        return l.get(0);
322                }
323        }
324
325        /**
326         * Get the names of all of the headers.
327         * 
328         * @return The header names
329         */
330        @Override
331        public Set<String> getHeaderNames()
332        {
333                return headers.keySet();
334        }
335
336        /**
337         * Get the encoded locale
338         * 
339         * @return The locale
340         */
341        @Override
342        public Locale getLocale()
343        {
344                return locale;
345        }
346
347        /**
348         * Get the output stream for writing binary data from the servlet.
349         * 
350         * @return The binary output stream.
351         */
352        @Override
353        public ServletOutputStream getOutputStream()
354        {
355                if (mode == MODE_TEXT)
356                {
357                        throw new IllegalArgumentException("Can't write binary after already selecting text");
358                }
359                mode = MODE_BINARY;
360                return servletStream;
361        }
362
363        /**
364         * Get the location that was redirected to.
365         * 
366         * @return The redirect location, or null if not a redirect
367         */
368        public String getRedirectLocation()
369        {
370                return redirectLocation;
371        }
372
373        /**
374         * Get the status code.
375         * 
376         * @return The status code
377         */
378        @Override
379        public int getStatus()
380        {
381                return status;
382        }
383
384        /**
385         * Get the print writer for writing text output for this response.
386         * 
387         * @return The writer
388         * @throws IOException
389         *             Not used
390         */
391        @Override
392        public PrintWriter getWriter() throws IOException
393        {
394                if (mode == MODE_BINARY)
395                {
396                        throw new IllegalArgumentException("Can't write text after already selecting binary");
397                }
398                mode = MODE_TEXT;
399                return printWriter;
400        }
401
402        /**
403         * Reset the response ready for reuse.
404         */
405        public void initialize()
406        {
407                cookies.clear();
408                headers.clear();
409                errorMessage = null;
410                redirectLocation = null;
411                status = HttpServletResponse.SC_OK;
412                characterEncoding = "UTF-8";
413                locale = null;
414
415                byteStream = new ByteArrayOutputStream();
416                servletStream = new ServletOutputStream()
417                {
418                        @Override
419                        public boolean isReady()
420                        {
421                                return true;
422                        }
423
424                        @Override
425                        public void setWriteListener(WriteListener writeListener)
426                        {
427                        }
428
429                        @Override
430                        public void write(int b)
431                        {
432                                byteStream.write(b);
433                        }
434                };
435                stringWriter = new StringWriter();
436                printWriter = new PrintWriter(stringWriter)
437                {
438                        @Override
439                        public void close()
440                        {
441                                // Do nothing
442                        }
443
444                        @Override
445                        public void flush()
446                        {
447                                // Do nothing
448                        }
449                };
450                mode = MODE_NONE;
451        }
452
453        /**
454         * Always returns false.
455         * 
456         * @return Always false
457         */
458        @Override
459        public boolean isCommitted()
460        {
461                return false;
462        }
463
464        /**
465         * Return whether the servlet returned an error code or not.
466         * 
467         * @return Whether an error occurred or not
468         */
469        public boolean isError()
470        {
471                return (status != HttpServletResponse.SC_OK);
472        }
473
474        /**
475         * Check whether the response was redirected or not.
476         * 
477         * @return Whether the state was redirected or not
478         */
479        public boolean isRedirect()
480        {
481                return (redirectLocation != null);
482        }
483
484        /**
485         * Delegate to initialize method.
486         */
487        @Override
488        public void reset()
489        {
490                initialize();
491        }
492
493        /**
494         * Clears the buffer.
495         */
496        @Override
497        public void resetBuffer()
498        {
499                if (mode == MODE_BINARY)
500                {
501                        byteStream.reset();
502                }
503                else if (mode == MODE_TEXT)
504                {
505                        stringWriter.getBuffer().delete(0, stringWriter.getBuffer().length());
506                }
507        }
508
509        /**
510         * Send an error code. This implementation just sets the internal error state information.
511         * 
512         * @param code
513         *            The code
514         * @throws IOException
515         *             Not used
516         */
517        @Override
518        public void sendError(final int code) throws IOException
519        {
520                status = code;
521                errorMessage = null;
522        }
523
524        /**
525         * Send an error code. This implementation just sets the internal error state information.
526         * 
527         * @param code
528         *            The error code
529         * @param msg
530         *            The error message
531         * @throws IOException
532         *             Not used
533         */
534        @Override
535        public void sendError(final int code, final String msg) throws IOException
536        {
537                status = code;
538                errorMessage = msg;
539        }
540
541        /**
542         * Indicate sending of a redirectLocation to a particular named resource. This implementation
543         * just keeps hold of the redirectLocation info and makes it available for query.
544         * 
545         * @param location
546         *            The location to redirectLocation to
547         * @throws IOException
548         *             Not used
549         */
550        @Override
551        public void sendRedirect(String location) throws IOException
552        {
553                redirectLocation = location;
554                status = HttpServletResponse.SC_FOUND;
555        }
556
557        /**
558         * Method ignored.
559         * 
560         * @param size
561         *            The size
562         */
563        @Override
564        public void setBufferSize(final int size)
565        {
566        }
567
568        /**
569         * Set the character encoding.
570         * 
571         * @param characterEncoding
572         *            The character encoding
573         */
574        @Override
575        public void setCharacterEncoding(final String characterEncoding)
576        {
577                this.characterEncoding = characterEncoding;
578        }
579
580        /**
581         * Set the content length.
582         * 
583         * @param length
584         *            The length
585         */
586        @Override
587        public void setContentLength(final int length)
588        {
589                setIntHeader("Content-Length", length);
590        }
591
592        @Override
593        public void setContentLengthLong(long len)
594        {
595                setContentLength((int) len);
596        }
597
598        /**
599         * Set the content type.
600         * 
601         * @param type
602         *            The content type
603         */
604        @Override
605        public void setContentType(final String type)
606        {
607                setHeader("Content-Type", type);
608        }
609
610        /**
611         * @return value of content-type header
612         */
613        @Override
614        public String getContentType()
615        {
616                return getHeader("Content-Type");
617        }
618
619        /**
620         * Set a date header.
621         * 
622         * @param name
623         *            The header name
624         * @param l
625         *            The long value
626         */
627        @Override
628        public void setDateHeader(final String name, final long l)
629        {
630                setHeader(name, formatDate(l));
631        }
632
633        /**
634         * @param l
635         * @return formatted date
636         */
637        public static String formatDate(long l)
638        {
639                StringBuilder _dateBuffer = new StringBuilder(32);
640                Calendar _calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
641                _calendar.setTimeInMillis(l);
642                formatDate(_dateBuffer, _calendar, false);
643                return _dateBuffer.toString();
644        }
645
646        /* BEGIN: This code comes from Jetty 6.1.1 */
647        private static String[] DAYS = { "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
648        private static String[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
649                        "Sep", "Oct", "Nov", "Dec", "Jan" };
650
651        /**
652         * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'" or "EEE, dd-MMM-yy HH:mm:ss 'GMT'"for
653         * cookies
654         * 
655         * @param buf
656         * @param calendar
657         * @param cookie
658         */
659        public static void formatDate(StringBuilder buf, Calendar calendar, boolean cookie)
660        {
661                // "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
662                // "EEE, dd-MMM-yy HH:mm:ss 'GMT'", cookie
663
664                int day_of_week = calendar.get(Calendar.DAY_OF_WEEK);
665                int day_of_month = calendar.get(Calendar.DAY_OF_MONTH);
666                int month = calendar.get(Calendar.MONTH);
667                int year = calendar.get(Calendar.YEAR);
668                int century = year / 100;
669                year = year % 100;
670
671                int epoch = (int)((calendar.getTimeInMillis() / 1000) % (60 * 60 * 24));
672                int seconds = epoch % 60;
673                epoch = epoch / 60;
674                int minutes = epoch % 60;
675                int hours = epoch / 60;
676
677                buf.append(DAYS[day_of_week]);
678                buf.append(',');
679                buf.append(' ');
680                append2digits(buf, day_of_month);
681
682                if (cookie)
683                {
684                        buf.append('-');
685                        buf.append(MONTHS[month]);
686                        buf.append('-');
687                        append2digits(buf, year);
688                }
689                else
690                {
691                        buf.append(' ');
692                        buf.append(MONTHS[month]);
693                        buf.append(' ');
694                        append2digits(buf, century);
695                        append2digits(buf, year);
696                }
697                buf.append(' ');
698                append2digits(buf, hours);
699                buf.append(':');
700                append2digits(buf, minutes);
701                buf.append(':');
702                append2digits(buf, seconds);
703                buf.append(" GMT");
704        }
705
706        /**
707         * @param buf
708         * @param i
709         */
710        public static void append2digits(StringBuilder buf, int i)
711        {
712                if (i < 100)
713                {
714                        buf.append((char)(i / 10 + '0'));
715                        buf.append((char)(i % 10 + '0'));
716                }
717        }
718
719        /* END: This code comes from Jetty 6.1.1 */
720
721        /**
722         * Set the given header value.
723         * 
724         * @param name
725         *            The name for the header
726         * @param value
727         *            The value for the header
728         */
729        @Override
730        public void setHeader(final String name, final String value)
731        {
732                List<String> l = new ArrayList<String>(1);
733                l.add(value);
734                headers.put(name, l);
735        }
736
737        /**
738         * Set an int header value.
739         * 
740         * @param name
741         *            The header name
742         * @param i
743         *            The value
744         */
745        @Override
746        public void setIntHeader(final String name, final int i)
747        {
748                setHeader(name, "" + i);
749        }
750
751        /**
752         * Set the locale in the response header.
753         * 
754         * @param locale
755         *            The locale
756         */
757        @Override
758        public void setLocale(final Locale locale)
759        {
760                this.locale = locale;
761        }
762
763        /**
764         * Set the status for this response.
765         * 
766         * @param status
767         *            The status
768         */
769        @Override
770        public void setStatus(final int status)
771        {
772                this.status = status;
773        }
774
775        /**
776         * @return binary response
777         */
778        public String getBinaryResponse()
779        {
780                String ctheader = getHeader("Content-Length");
781                if (ctheader == null)
782                {
783                        return getDocument();
784                }
785                else
786                {
787                        return getDocument().substring(0, Integer.valueOf(ctheader));
788                }
789        }
790
791        /**
792         * @param name
793         * @return headers with given name
794         */
795        @Override
796        public Collection<String> getHeaders(String name)
797        {
798                return Collections.singletonList(headers.get(name).toString());
799        }
800
801        @Override
802        public void writeMetaData(WebResponse webResponse)
803        {
804                for (Cookie cookie : cookies)
805                {
806                        webResponse.addCookie(cookie);
807                }
808                for (String name : headers.keySet())
809                {
810                        webResponse.setHeader(name, headers.get(name).toString());
811                }
812                webResponse.setStatus(status);
813        }
814}