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