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.page;
018
019import java.time.Instant;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import javax.servlet.http.Cookie;
027
028import org.apache.wicket.Application;
029import org.apache.wicket.Component;
030import org.apache.wicket.Page;
031import org.apache.wicket.behavior.Behavior;
032import org.apache.wicket.feedback.FeedbackDelay;
033import org.apache.wicket.markup.head.HeaderItem;
034import org.apache.wicket.markup.head.IHeaderResponse;
035import org.apache.wicket.markup.head.IWrappedHeaderItem;
036import org.apache.wicket.markup.head.JavaScriptHeaderItem;
037import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
038import org.apache.wicket.markup.head.OnEventHeaderItem;
039import org.apache.wicket.markup.head.OnLoadHeaderItem;
040import org.apache.wicket.markup.head.internal.HeaderResponse;
041import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
042import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler;
043import org.apache.wicket.markup.renderStrategy.AbstractHeaderRenderStrategy;
044import org.apache.wicket.markup.renderStrategy.IHeaderRenderStrategy;
045import org.apache.wicket.markup.repeater.AbstractRepeater;
046import org.apache.wicket.request.IRequestCycle;
047import org.apache.wicket.request.Response;
048import org.apache.wicket.request.cycle.RequestCycle;
049import org.apache.wicket.request.http.WebResponse;
050import org.apache.wicket.response.StringResponse;
051import org.apache.wicket.util.lang.Args;
052import org.apache.wicket.util.lang.Classes;
053import org.apache.wicket.util.lang.Generics;
054import org.apache.wicket.util.string.AppendingStringBuffer;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058/**
059 * A partial update of a page that collects components and header contributions to be written to the
060 * client in a specific String-based format (XML, JSON, * ...).
061 * <p>
062 * The elements of such response are:
063 * <ul>
064 * <li>component - the markup of the updated component</li>
065 * <li>header-contribution - all HeaderItems which have been contributed in any{@link Component#renderHead(IHeaderResponse)},
066 * {@link Behavior#renderHead(Component, IHeaderResponse)} or JavaScript explicitly added via {@link #appendJavaScript(CharSequence)}
067 * or {@link #prependJavaScript(CharSequence)}</li>
068 * </ul>
069 */
070public abstract class PartialPageUpdate
071{
072        private static final Logger LOG = LoggerFactory.getLogger(PartialPageUpdate.class);
073
074        /**
075         * A list of scripts (JavaScript) which should be executed on the client side before the
076         * components' replacement
077         */
078        protected final List<CharSequence> prependJavaScripts = Generics.newArrayList();
079
080        /**
081         * A list of scripts (JavaScript) which should be executed on the client side after the
082         * components' replacement
083         */
084        protected final List<CharSequence> appendJavaScripts = Generics.newArrayList();
085
086        /**
087         * A list of scripts (JavaScript) which should be executed on the client side after the
088         * components' replacement.
089         * Executed immediately after the replacement of the components, and before appendJavaScripts
090         */
091        protected final List<CharSequence> domReadyJavaScripts = Generics.newArrayList();
092
093        /**
094         * The component instances that will be rendered/replaced.
095         */
096        protected final Map<String, Component> markupIdToComponent = new LinkedHashMap<>();
097
098        /**
099         * A flag that indicates that components cannot be added anymore.
100         * See https://issues.apache.org/jira/browse/WICKET-3564
101         * 
102         * @see #add(Component, String)
103         */
104        protected transient boolean componentsFrozen;
105
106        /**
107         * A flag that indicates that javascripts cannot be added anymore.
108         * See https://issues.apache.org/jira/browse/WICKET-6902
109         */
110        protected transient boolean javascriptsFrozen;
111
112        /**
113         * Buffer of response body. 
114         */
115        protected final ResponseBuffer bodyBuffer;
116
117        /**
118         * Buffer of response header.
119         */
120        protected final ResponseBuffer headerBuffer;
121
122        protected HtmlHeaderContainer header = null;
123        
124        private Component originalHeaderContainer;
125
126        // whether a header contribution is being rendered
127        private boolean headerRendering = false;
128
129        private IHeaderResponse headerResponse;
130
131        /**
132         * The page which components are being updated.
133         */
134        private final Page page;
135        
136        /**
137         * Constructor.
138         *
139         * @param page
140         *      the page which components are being updated.
141         */
142        public PartialPageUpdate(final Page page)
143        {
144                this.page = page;
145                this.originalHeaderContainer = page.get(HtmlHeaderSectionHandler.HEADER_ID);
146                
147                WebResponse response = (WebResponse) page.getResponse();
148                bodyBuffer = new ResponseBuffer(response);
149                headerBuffer = new ResponseBuffer(response);
150        }
151
152        /**
153         * @return returns true if and only if nothing has being added to partial update.
154         */
155        public boolean isEmpty()
156        {
157                return prependJavaScripts.isEmpty() && appendJavaScripts.isEmpty() && domReadyJavaScripts.isEmpty() && markupIdToComponent.isEmpty();
158        }
159
160        /**
161         * Serializes this object to the response.
162         *
163         * @param response
164         *      the response to write to
165         * @param encoding
166         *      the encoding for the response
167         */
168        public void writeTo(final Response response, final String encoding)
169        {
170                try {
171                        writeHeader(response, encoding);
172
173                        onBeforeRespond(response);
174
175                        // process added components
176                        writeComponents(response, encoding);
177
178                        onAfterRespond(response);
179                        
180                        javascriptsFrozen = true;
181
182                        // queue up prepend javascripts. unlike other steps these are executed out of order so that
183                        // components can contribute them from during rendering.
184                        writePriorityEvaluations(response, prependJavaScripts);
185
186                        // execute the dom ready javascripts as first javascripts
187                        // after component replacement
188                        List<CharSequence> evaluationScripts = new ArrayList<>();
189                        evaluationScripts.addAll(domReadyJavaScripts);
190                        evaluationScripts.addAll(appendJavaScripts);
191                        writeEvaluations(response, evaluationScripts);
192
193                        writeFooter(response, encoding);
194                } finally {
195                        if (header != null && originalHeaderContainer!= null) {
196                                // restore a normal header
197                                page.replace(originalHeaderContainer);
198                                header = null;
199                        }
200                }
201        }
202
203        /**
204         * Hook-method called before components are written. 
205         * 
206         * @param response
207         */
208        protected void onBeforeRespond(Response response) {
209        }
210
211        /**
212         * Hook-method called after components are written. 
213         * 
214         * @param response
215         */
216        protected void onAfterRespond(Response response) {
217        }
218
219        /**
220         * @param response
221         *      the response to write to
222         * @param encoding
223         *      the encoding for the response
224         */
225    protected abstract void writeFooter(Response response, String encoding);
226
227        /**
228         *
229         * @param response
230         *      the response to write to
231         * @param scripts
232         *      the JavaScripts to evaluate
233         */
234        protected void writePriorityEvaluations(final Response response, Collection<CharSequence> scripts)
235        {
236                if (!scripts.isEmpty())
237                {
238                        CharSequence contents = renderScripts(scripts);
239                        
240                        writePriorityEvaluation(response, contents);
241                }
242        }
243        
244        /**
245         *
246         * @param response
247         *      the response to write to
248         * @param scripts
249         *      the JavaScripts to evaluate
250         */
251        protected void writeEvaluations(final Response response, Collection<CharSequence> scripts)
252        {
253                if (!scripts.isEmpty())
254                {
255                        CharSequence contents = renderScripts(scripts);
256                        
257                        writeEvaluation(response, contents);
258                }
259        }
260
261        private CharSequence renderScripts(Collection<CharSequence> scripts) {
262                StringBuilder combinedScript = new StringBuilder(1024);
263                for (CharSequence script : scripts)
264                {
265                        combinedScript.append("(function(){").append(script).append("})();");
266                }
267
268                StringResponse stringResponse = new StringResponse();
269                IHeaderResponse decoratedHeaderResponse = Application.get().decorateHeaderResponse(new HeaderResponse()
270                {
271                        @Override
272                        protected Response getRealResponse()
273                        {
274                                return stringResponse;
275                        }
276                });
277                
278                decoratedHeaderResponse.render(JavaScriptHeaderItem.forScript(combinedScript, null));
279                decoratedHeaderResponse.close();
280                
281                return stringResponse.getBuffer();
282        }
283
284        /**
285         * Processes components added to the target. This involves attaching components, rendering
286         * markup into a client side xml envelope, and detaching them
287         *
288         * @param response
289         *      the response to write to
290         * @param encoding
291         *      the encoding for the response
292         */
293        private void writeComponents(Response response, String encoding)
294        {
295                componentsFrozen = true;
296
297                List<Component> toBeWritten = new ArrayList<>(markupIdToComponent.size());
298                
299                // delay preparation of feedbacks after all other components
300                try (FeedbackDelay delay = new FeedbackDelay(RequestCycle.get())) {
301                        for (Component component : markupIdToComponent.values())
302                        {
303                                if (!containsAncestorFor(component) && prepareComponent(component)) {
304                                        toBeWritten.add(component);
305                                }
306                        }
307
308                        // .. now prepare all postponed feedbacks
309                        delay.beforeRender();
310                }
311
312                // write components
313                for (Component component : toBeWritten)
314                {
315                        writeComponent(response, component.getAjaxRegionMarkupId(), component, encoding);
316                }
317
318                if (header != null)
319                {
320                        RequestCycle cycle = RequestCycle.get();
321                        
322                        // some header responses buffer all calls to render*** until close is called.
323                        // when they are closed, they do something (i.e. aggregate all JS resource urls to a
324                        // single url), and then "flush" (by writing to the real response) before closing.
325                        // to support this, we need to allow header contributions to be written in the close
326                        // tag, which we do here:
327                        headerRendering = true;
328                        // save old response, set new
329                        Response oldResponse = cycle.setResponse(headerBuffer);
330                        headerBuffer.reset();
331
332                        // now, close the response (which may render things)
333                        header.getHeaderResponse().close();
334
335                        // revert to old response
336                        cycle.setResponse(oldResponse);
337
338                        // write the XML tags and we're done
339                        writeHeaderContribution(response, headerBuffer.getContents());
340                        headerRendering = false;
341                }
342        }
343
344        /**
345         * Prepare a single component
346         *
347         * @param component
348         *      the component to prepare
349         * @return whether the component was prepared
350         */
351        protected boolean prepareComponent(Component component)
352        {
353                if (component.getRenderBodyOnly())
354                {
355                        throw new IllegalStateException(
356                                        "A partial update is not possible for a component that has renderBodyOnly enabled. Component: " +
357                                                        component.toString());
358                }
359
360                component.setOutputMarkupId(true);
361
362                // Initialize temporary variables
363                final Page parentPage = component.findParent(Page.class);
364                if (parentPage == null)
365                {
366                        // dont throw an exception but just ignore this component, somehow
367                        // it got removed from the page.
368                        LOG.warn("Component '{}' not rendered because it was already removed from page", component);
369                        return false;
370                }
371
372                try
373                {
374                        component.beforeRender();
375                }
376                catch (RuntimeException e)
377                {
378                        bodyBuffer.reset();
379                        throw e;
380                }
381                
382                return true;
383        }
384
385        /**
386         * Writes a single component
387         *
388         * @param response
389         *      the response to write to
390         * @param markupId
391         *      the markup id to use for the component replacement
392         * @param component
393         *      the component which markup will be used as replacement
394         * @param encoding
395         *      the encoding for the response
396         */
397        protected void writeComponent(Response response, String markupId, Component component, String encoding)
398        {
399                // substitute our encoding response for the old one so we can capture
400                // component's markup in a manner safe for transport inside CDATA block
401                Response oldResponse = RequestCycle.get().setResponse(bodyBuffer);
402
403                try
404                {
405                        // render any associated headers of the component
406                        writeHeaderContribution(response, component);
407                        
408                        bodyBuffer.reset();
409                        
410                        try
411                        {
412                                component.renderPart();
413                        }
414                        catch (RuntimeException e)
415                        {
416                                bodyBuffer.reset();
417                                throw e;
418                        }
419                }
420                finally
421                {
422                        // Restore original response
423                        RequestCycle.get().setResponse(oldResponse);
424                }
425
426                writeComponent(response, markupId, bodyBuffer.getContents());
427
428                bodyBuffer.reset();
429        }
430
431        /**
432         * Writes the head part of the response.
433         * For example XML preamble
434         *
435         * @param response
436         *      the response to write to
437         * @param encoding
438         *      the encoding for the response
439         */
440        protected abstract void writeHeader(Response response, String encoding);
441
442        /**
443         * Writes a component to the response.
444         * <p>
445         * TODO make abstract in Wicket 10
446         *
447         * @param response
448         *      the response to write to
449         * @param contents      
450         *              the contents
451         */
452        protected void writeComponent(Response response, String markupId, CharSequence contents) {
453                throw new UnsupportedOperationException();
454        }
455
456        /**
457         * TODO make abstract in Wicket 10
458         */
459        protected void writePriorityEvaluation(Response response, CharSequence contents) {
460                throw new UnsupportedOperationException();              
461        }
462
463        /**
464         * Writes a header contribution to the response.
465         *
466         * @param response
467         *      the response to write to
468         * @param contents      
469         *              the contents
470         */
471        protected abstract void writeHeaderContribution(Response response, CharSequence contents);
472
473        /**
474         * TODO make abstract in Wicket 10
475         */
476        protected void writeEvaluation(Response response, CharSequence contents) {
477                throw new UnsupportedOperationException();              
478        }
479
480        @Override
481        public boolean equals(Object o)
482        {
483                if (this == o) return true;
484                if (o == null || getClass() != o.getClass()) return false;
485
486                PartialPageUpdate that = (PartialPageUpdate) o;
487
488                if (!appendJavaScripts.equals(that.appendJavaScripts)) return false;
489                if (!domReadyJavaScripts.equals(that.domReadyJavaScripts)) return false;
490                return prependJavaScripts.equals(that.prependJavaScripts);
491        }
492
493        @Override
494        public int hashCode()
495        {
496                int result = prependJavaScripts.hashCode();
497                result = 31 * result + appendJavaScripts.hashCode();
498                result = 31 * result + domReadyJavaScripts.hashCode();
499                return result;
500        }
501
502        /**
503         * Adds script to the ones which are executed after the component replacement.
504         *
505         * @param javascript
506         *      the javascript to execute
507         */
508        public final void appendJavaScript(final CharSequence javascript)
509        {
510                Args.notNull(javascript, "javascript");
511
512                if (javascriptsFrozen)
513                {
514                        throw new IllegalStateException("A partial update of the page is being rendered, JavaScript can no longer be added");
515                }
516
517                appendJavaScripts.add(javascript);
518        }
519
520        /**
521         * Adds script to the ones which are executed before the component replacement.
522         *
523         * @param javascript
524         *      the javascript to execute
525         */
526        public final void prependJavaScript(CharSequence javascript)
527        {
528                Args.notNull(javascript, "javascript");
529                
530                if (javascriptsFrozen)
531                {
532                        throw new IllegalStateException("A partial update of the page is being rendered, JavaScript can no longer be added");
533                }
534
535                prependJavaScripts.add(javascript);
536        }
537
538        /**
539         * Adds a component to be updated at the client side with its current markup
540         *
541         * @param component
542         *      the component to update
543         * @param markupId
544         *      the markup id to use to find the component in the page's markup
545         * @throws IllegalArgumentException
546         *      thrown when a Page or an AbstractRepeater is added
547         * @throws IllegalStateException
548         *      thrown when components no more can be added for replacement.
549         */
550        public final void add(final Component component, final String markupId)
551        {
552                Args.notEmpty(markupId, "markupId");
553                Args.notNull(component, "component");
554
555                if (component instanceof Page)
556                {
557                        if (component != page)
558                        {
559                                throw new IllegalArgumentException("Cannot add another page");
560                        }
561                }
562                else
563                {
564                        Page pageOfComponent = component.findParent(Page.class);
565                        if (pageOfComponent == null) 
566                        {
567                                // no longer on page - log the error but don't block the user of the application
568                                // (which was the behavior in Wicket <= 7).
569                                LOG.warn("Component '{}' not cannot be updated because it was already removed from page", component);
570                                return;
571                        }
572                        else if (pageOfComponent != page) 
573                        {
574                                // on another page
575                                throw new IllegalArgumentException("Component " + component.toString() + " cannot be updated because it is on another page.");
576                        }
577
578                        if (component instanceof AbstractRepeater)
579                        {
580                                throw new IllegalArgumentException(
581                                        "Component " +
582                                        Classes.name(component.getClass()) +
583                                        " is a repeater and cannot be added to a partial page update directly. " +
584                                        "Instead add its parent or another markup container higher in the hierarchy.");
585                        }
586                }
587
588                if (componentsFrozen)
589                {
590                        throw new IllegalStateException("A partial update of the page is being rendered, component " + component.toString() + " can no longer be added");
591                }
592
593                component.setMarkupId(markupId);
594                markupIdToComponent.put(markupId, component);
595        }
596
597        /**
598         * @return a read-only collection of all components which have been added for replacement so far.
599         */
600        public final Collection<? extends Component> getComponents()
601        {
602                return Collections.unmodifiableCollection(markupIdToComponent.values());
603        }
604
605        /**
606         * Detaches the page if at least one of its components was updated.
607         *
608         * @param requestCycle
609         *      the current request cycle
610         */
611        public void detach(IRequestCycle requestCycle)
612        {
613                for (final Component component : markupIdToComponent.values()) {
614                        final Page parentPage = component.findParent(Page.class);
615                        if (parentPage != null) {
616                                parentPage.detach();
617                                break;
618                        }
619                }
620        }
621
622        /**
623         * Checks if the target contains an ancestor for the given component
624         *
625         * @param component
626         *      the component which ancestors should be checked.
627         * @return <code>true</code> if target contains an ancestor for the given component
628         */
629        protected boolean containsAncestorFor(Component component)
630        {
631                Component cursor = component.getParent();
632                while (cursor != null)
633                {
634                        if (markupIdToComponent.containsValue(cursor))
635                        {
636                                return true;
637                        }
638                        cursor = cursor.getParent();
639                }
640                return false;
641        }
642
643        /**
644         * @return {@code true} if the page has been added for replacement
645         */
646        public boolean containsPage()
647        {
648                return markupIdToComponent.containsValue(page);
649        }
650
651        /**
652         * Gets or creates an IHeaderResponse instance to use for the header contributions.
653         *
654         * @return IHeaderResponse instance to use for the header contributions.
655         */
656        public IHeaderResponse getHeaderResponse()
657        {
658                if (headerResponse == null)
659                {
660                        // we don't need to decorate the header response here because this is called from
661                        // within PartialHtmlHeaderContainer, which decorates the response
662                        headerResponse = new PartialHeaderResponse();
663                }
664                return headerResponse;
665        }
666
667        /**
668         * @param response
669         *      the response to write to
670         * @param component
671         *      to component which will contribute to the header
672         */
673        protected void writeHeaderContribution(final Response response, final Component component)
674        {
675                headerRendering = true;
676
677                // create the htmlheadercontainer if needed
678                if (header == null)
679                {
680                        header = new PartialHtmlHeaderContainer(this);
681                        page.addOrReplace(header);
682                }
683
684                RequestCycle requestCycle = component.getRequestCycle();
685
686                // save old response, set new
687                Response oldResponse = requestCycle.setResponse(headerBuffer);
688
689                try {
690                        headerBuffer.reset();
691
692                        IHeaderRenderStrategy strategy = AbstractHeaderRenderStrategy.get();
693
694                        strategy.renderHeader(header, null, component);
695                } finally {
696                        // revert to old response
697                        requestCycle.setResponse(oldResponse);
698                }
699
700                // note: in almost all cases the header will be empty here,
701                // since all header items will be rendered later on close only
702                writeHeaderContribution(response, headerBuffer.getContents());
703                headerRendering = false;
704        }
705
706        /**
707         * Sets the Content-Type header to indicate the type of the response.
708         *
709         * @param response
710         *      the current we response
711         * @param encoding
712         *      the encoding to use
713         */
714        public abstract void setContentType(WebResponse response, String encoding);
715
716        /**
717         * Header container component for partial page updates.
718         * <p>
719         * This container is temporarily injected into the page to provide the
720         * {@link IHeaderResponse} while components are rendered. It is never
721         * rendered itself. 
722         *
723         * @author Matej Knopp
724         */
725        private static class PartialHtmlHeaderContainer extends HtmlHeaderContainer
726        {
727                private static final long serialVersionUID = 1L;
728
729                /**
730                 * Keep transiently, in case the containing page gets serialized before
731                 * this container is removed again. This happens when DebugBar determines
732                 * the page size by serializing/deserializing it.
733                 */
734                private transient PartialPageUpdate pageUpdate;
735
736                /**
737                 * Constructor.
738                 *
739                 * @param pageUpdate
740                 *      the partial page update
741                 */
742                public PartialHtmlHeaderContainer(PartialPageUpdate pageUpdate)
743                {
744                        super(HtmlHeaderSectionHandler.HEADER_ID);
745
746                        this.pageUpdate = pageUpdate;
747                }
748
749                /**
750                 *
751                 * @see org.apache.wicket.markup.html.internal.HtmlHeaderContainer#newHeaderResponse()
752                 */
753                @Override
754                protected IHeaderResponse newHeaderResponse()
755                {
756                        if (pageUpdate == null) {
757                                throw new IllegalStateException("disconnected from pageUpdate after serialization");
758                        }
759
760                return pageUpdate.getHeaderResponse();
761                }
762        }
763
764        /**
765         * Header response for partial updates.
766         *
767         * @author Matej Knopp
768         */
769        private class PartialHeaderResponse extends HeaderResponse
770        {
771                @Override
772                public void render(HeaderItem item)
773                {
774                        while (item instanceof IWrappedHeaderItem)
775                        {
776                                item = ((IWrappedHeaderItem) item).getWrapped();
777                        }
778
779                        if (item instanceof OnLoadHeaderItem)
780                        {
781                                if (!wasItemRendered(item))
782                                {
783                                        PartialPageUpdate.this.appendJavaScript(((OnLoadHeaderItem) item).getJavaScript());
784                                        markItemRendered(item);
785                                }
786                        }
787                        else if (item instanceof OnEventHeaderItem)
788                        {
789                                if (!wasItemRendered(item))
790                                {
791                                        PartialPageUpdate.this.appendJavaScript(((OnEventHeaderItem) item).getCompleteJavaScript());
792                                        markItemRendered(item);
793                                }
794                        }
795                        else if (item instanceof OnDomReadyHeaderItem)
796                        {
797                                if (!wasItemRendered(item))
798                                {
799                                        PartialPageUpdate.this.domReadyJavaScripts.add(((OnDomReadyHeaderItem)item).getJavaScript());
800                                        markItemRendered(item);
801                                }
802                        }
803                        else if (headerRendering)
804                        {
805                                super.render(item);
806                        }
807                        else
808                        {
809                                LOG.debug("Only methods that can be called on IHeaderResponse outside renderHead() are #render(OnLoadHeaderItem) and #render(OnDomReadyHeaderItem)");
810                        }
811                }
812
813                @Override
814                protected Response getRealResponse()
815                {
816                        return RequestCycle.get().getResponse();
817                }
818        }
819
820        /**
821         * Wrapper of a response that buffers its contents.
822         *
823         * @author Igor Vaynberg (ivaynberg)
824         * @author Sven Meier (svenmeier)
825         * 
826         * @see ResponseBuffer#getContents()
827         * @see ResponseBuffer#reset()
828         */
829        protected static final class ResponseBuffer extends WebResponse
830        {
831                private final AppendingStringBuffer buffer = new AppendingStringBuffer(256);
832
833                private final WebResponse originalResponse;
834
835                /**
836                 * Constructor.
837                 *
838                 * @param originalResponse
839                 *      the original request cycle response
840                 */
841                private ResponseBuffer(WebResponse originalResponse)
842                {
843                        this.originalResponse = originalResponse;
844                }
845
846                /**
847                 * @see org.apache.wicket.request.Response#encodeURL(CharSequence)
848                 */
849                @Override
850                public String encodeURL(CharSequence url)
851                {
852                        return originalResponse.encodeURL(url);
853                }
854
855                /**
856                 * @return contents of the response
857                 */
858                public CharSequence getContents()
859                {
860                        return buffer;
861                }
862
863                /**
864                 * @see org.apache.wicket.request.Response#write(CharSequence)
865                 */
866                @Override
867                public void write(CharSequence cs)
868                {
869                        buffer.append(cs);
870                }
871
872                /**
873                 * Resets the response to a clean state so it can be reused to save on garbage.
874                 */
875                @Override
876                public void reset()
877                {
878                        buffer.clear();
879                }
880
881                @Override
882                public void write(byte[] array)
883                {
884                        throw new UnsupportedOperationException("Cannot write binary data.");
885                }
886
887                @Override
888                public void write(byte[] array, int offset, int length)
889                {
890                        throw new UnsupportedOperationException("Cannot write binary data.");
891                }
892
893                @Override
894                public Object getContainerResponse()
895                {
896                        return originalResponse.getContainerResponse();
897                }
898
899                @Override
900                public void addCookie(Cookie cookie)
901                {
902                        originalResponse.addCookie(cookie);
903                }
904
905                @Override
906                public void clearCookie(Cookie cookie)
907                {
908                        originalResponse.clearCookie(cookie);
909                }
910
911                @Override
912                public boolean isHeaderSupported()
913                {
914                        return originalResponse.isHeaderSupported();
915                }
916
917                @Override
918                public void setHeader(String name, String value)
919                {
920                        originalResponse.setHeader(name, value);
921                }
922
923                @Override
924                public void addHeader(String name, String value)
925                {
926                        originalResponse.addHeader(name, value);
927                }
928
929                @Override
930                public void setDateHeader(String name, Instant date)
931                {
932                        originalResponse.setDateHeader(name, date);
933                }
934
935                @Override
936                public void setContentLength(long length)
937                {
938                        originalResponse.setContentLength(length);
939                }
940
941                @Override
942                public void setContentType(String mimeType)
943                {
944                        originalResponse.setContentType(mimeType);
945                }
946
947                @Override
948                public void setStatus(int sc)
949                {
950                        originalResponse.setStatus(sc);
951                }
952
953                @Override
954                public void sendError(int sc, String msg)
955                {
956                        originalResponse.sendError(sc, msg);
957                }
958
959                @Override
960                public String encodeRedirectURL(CharSequence url)
961                {
962                        return originalResponse.encodeRedirectURL(url);
963                }
964
965                @Override
966                public void sendRedirect(String url)
967                {
968                        originalResponse.sendRedirect(url);
969                }
970
971                @Override
972                public boolean isRedirect()
973                {
974                        return originalResponse.isRedirect();
975                }
976
977                @Override
978                public void flush()
979                {
980                        originalResponse.flush();
981                }
982        }
983}