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         * 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         * <p>
451         * TODO make abstract in Wicket 10
452         *
453         * @param response
454         *      the response to write to
455         * @param contents      
456         *              the contents
457         */
458        protected void writeComponent(Response response, String markupId, CharSequence contents) {
459                throw new UnsupportedOperationException();
460        }
461
462        /**
463         * TODO make abstract in Wicket 10
464         */
465        protected void writePriorityEvaluation(Response response, CharSequence contents) {
466                throw new UnsupportedOperationException();              
467        }
468
469        /**
470         * Writes a header contribution to the response.
471         *
472         * @param response
473         *      the response to write to
474         * @param contents      
475         *              the contents
476         */
477        protected abstract void writeHeaderContribution(Response response, CharSequence contents);
478
479        /**
480         * TODO make abstract in Wicket 10
481         */
482        protected void writeEvaluation(Response response, CharSequence contents) {
483                throw new UnsupportedOperationException();              
484        }
485
486        @Override
487        public boolean equals(Object o)
488        {
489                if (this == o) return true;
490                if (o == null || getClass() != o.getClass()) return false;
491
492                PartialPageUpdate that = (PartialPageUpdate) o;
493
494                if (!appendJavaScripts.equals(that.appendJavaScripts)) return false;
495                if (!domReadyJavaScripts.equals(that.domReadyJavaScripts)) return false;
496                return prependJavaScripts.equals(that.prependJavaScripts);
497        }
498
499        @Override
500        public int hashCode()
501        {
502                int result = prependJavaScripts.hashCode();
503                result = 31 * result + appendJavaScripts.hashCode();
504                result = 31 * result + domReadyJavaScripts.hashCode();
505                return result;
506        }
507
508        /**
509         * Adds script to the ones which are executed after the component replacement.
510         *
511         * @param javascript
512         *      the javascript to execute
513         */
514        public final void appendJavaScript(final CharSequence javascript)
515        {
516                Args.notNull(javascript, "javascript");
517
518                if (javascriptsFrozen)
519                {
520                        throw new IllegalStateException("A partial update of the page is being rendered, JavaScript can no longer be added");
521                }
522
523                appendJavaScripts.add(javascript);
524        }
525
526        /**
527         * Adds script to the ones which are executed before the component replacement.
528         *
529         * @param javascript
530         *      the javascript to execute
531         */
532        public final void prependJavaScript(CharSequence javascript)
533        {
534                Args.notNull(javascript, "javascript");
535                
536                if (javascriptsFrozen)
537                {
538                        throw new IllegalStateException("A partial update of the page is being rendered, JavaScript can no longer be added");
539                }
540
541                prependJavaScripts.add(javascript);
542        }
543
544        /**
545         * Adds a component to be updated at the client side with its current markup
546         *
547         * @param component
548         *      the component to update
549         * @param markupId
550         *      the markup id to use to find the component in the page's markup
551         * @throws IllegalArgumentException
552         *      thrown when a Page or an AbstractRepeater is added
553         * @throws IllegalStateException
554         *      thrown when components no more can be added for replacement.
555         */
556        public final void add(final Component component, final String markupId)
557        {
558                Args.notEmpty(markupId, "markupId");
559                Args.notNull(component, "component");
560
561                if (component instanceof Page)
562                {
563                        if (component != page)
564                        {
565                                throw new IllegalArgumentException("Cannot add another page");
566                        }
567                }
568                else
569                {
570                        Page pageOfComponent = component.findParent(Page.class);
571                        if (pageOfComponent == null) 
572                        {
573                                // no longer on page - log the error but don't block the user of the application
574                                // (which was the behavior in Wicket <= 7).
575                                LOG.warn("Component '{}' not cannot be updated because it was already removed from page", component);
576                                return;
577                        }
578                        else if (pageOfComponent != page) 
579                        {
580                                // on another page
581                                throw new IllegalArgumentException("Component " + component.toString() + " cannot be updated because it is on another page.");
582                        }
583
584                        if (component instanceof AbstractRepeater)
585                        {
586                                throw new IllegalArgumentException(
587                                        "Component " +
588                                        Classes.name(component.getClass()) +
589                                        " is a repeater and cannot be added to a partial page update directly. " +
590                                        "Instead add its parent or another markup container higher in the hierarchy.");
591                        }
592                }
593
594                if (componentsFrozen)
595                {
596                        throw new IllegalStateException("A partial update of the page is being rendered, component " + component.toString() + " can no longer be added");
597                }
598
599                component.setMarkupId(markupId);
600                markupIdToComponent.put(markupId, component);
601        }
602
603        /**
604         * @return a read-only collection of all components which have been added for replacement so far.
605         */
606        public final Collection<? extends Component> getComponents()
607        {
608                return Collections.unmodifiableCollection(markupIdToComponent.values());
609        }
610
611        /**
612         * Detaches the page if at least one of its components was updated.
613         *
614         * @param requestCycle
615         *      the current request cycle
616         */
617        public void detach(IRequestCycle requestCycle)
618        {
619                for (final Component component : markupIdToComponent.values()) {
620                        final Page parentPage = component.findParent(Page.class);
621                        if (parentPage != null) {
622                                parentPage.detach();
623                                break;
624                        }
625                }
626        }
627
628        /**
629         * Checks if the target contains an ancestor for the given component
630         *
631         * @param component
632         *      the component which ancestors should be checked.
633         * @return <code>true</code> if target contains an ancestor for the given component
634         */
635        protected boolean containsAncestorFor(Component component)
636        {
637                Component cursor = component.getParent();
638                while (cursor != null)
639                {
640                        if (markupIdToComponent.containsValue(cursor))
641                        {
642                                return true;
643                        }
644                        cursor = cursor.getParent();
645                }
646                return false;
647        }
648
649        /**
650         * @return {@code true} if the page has been added for replacement
651         */
652        public boolean containsPage()
653        {
654                return markupIdToComponent.containsValue(page);
655        }
656
657        /**
658         * Gets or creates an IHeaderResponse instance to use for the header contributions.
659         *
660         * @return IHeaderResponse instance to use for the header contributions.
661         */
662        public IHeaderResponse getHeaderResponse()
663        {
664                if (headerResponse == null)
665                {
666                        // we don't need to decorate the header response here because this is called from
667                        // within PartialHtmlHeaderContainer, which decorates the response
668                        headerResponse = new PartialHeaderResponse();
669                }
670                return headerResponse;
671        }
672
673        /**
674         * @param response
675         *      the response to write to
676         * @param component
677         *      to component which will contribute to the header
678         */
679        protected void writeHeaderContribution(final Response response, final Component component)
680        {
681                headerRendering = true;
682
683                // create the htmlheadercontainer if needed
684                if (header == null)
685                {
686                        header = new PartialHtmlHeaderContainer(this);
687                        page.addOrReplace(header);
688                }
689
690                RequestCycle requestCycle = component.getRequestCycle();
691
692                // save old response, set new
693                Response oldResponse = requestCycle.setResponse(headerBuffer);
694
695                try {
696                        headerBuffer.reset();
697
698                        IHeaderRenderStrategy strategy = AbstractHeaderRenderStrategy.get();
699
700                        strategy.renderHeader(header, null, component);
701                } finally {
702                        // revert to old response
703                        requestCycle.setResponse(oldResponse);
704                }
705
706                // note: in almost all cases the header will be empty here,
707                // since all header items will be rendered later on close only
708                writeHeaderContribution(response, headerBuffer.getContents());
709                headerRendering = false;
710        }
711
712        /**
713         * Sets the Content-Type header to indicate the type of the response.
714         *
715         * @param response
716         *      the current we response
717         * @param encoding
718         *      the encoding to use
719         */
720        public abstract void setContentType(WebResponse response, String encoding);
721
722        /**
723         * Header container component for partial page updates.
724         * <p>
725         * This container is temporarily injected into the page to provide the
726         * {@link IHeaderResponse} while components are rendered. It is never
727         * rendered itself. 
728         *
729         * @author Matej Knopp
730         */
731        private static class PartialHtmlHeaderContainer extends HtmlHeaderContainer
732        {
733                private static final long serialVersionUID = 1L;
734
735                /**
736                 * Keep transiently, in case the containing page gets serialized before
737                 * this container is removed again. This happens when DebugBar determines
738                 * the page size by serializing/deserializing it.
739                 */
740                private transient PartialPageUpdate pageUpdate;
741
742                /**
743                 * Constructor.
744                 *
745                 * @param pageUpdate
746                 *      the partial page update
747                 */
748                public PartialHtmlHeaderContainer(PartialPageUpdate pageUpdate)
749                {
750                        super(HtmlHeaderSectionHandler.HEADER_ID);
751
752                        this.pageUpdate = pageUpdate;
753                }
754
755                /**
756                 *
757                 * @see org.apache.wicket.markup.html.internal.HtmlHeaderContainer#newHeaderResponse()
758                 */
759                @Override
760                protected IHeaderResponse newHeaderResponse()
761                {
762                        if (pageUpdate == null) {
763                                throw new IllegalStateException("disconnected from pageUpdate after serialization");
764                        }
765
766                return pageUpdate.getHeaderResponse();
767                }
768        }
769
770        /**
771         * Header response for partial updates.
772         *
773         * @author Matej Knopp
774         */
775        private class PartialHeaderResponse extends HeaderResponse
776        {
777                @Override
778                public void render(HeaderItem item)
779                {
780                        while (item instanceof IWrappedHeaderItem)
781                        {
782                                item = ((IWrappedHeaderItem) item).getWrapped();
783                        }
784
785                        if (item instanceof OnLoadHeaderItem)
786                        {
787                                if (!wasItemRendered(item))
788                                {
789                                        PartialPageUpdate.this.appendJavaScript(((OnLoadHeaderItem) item).getJavaScript());
790                                        markItemRendered(item);
791                                }
792                        }
793                        else if (item instanceof OnEventHeaderItem)
794                        {
795                                if (!wasItemRendered(item))
796                                {
797                                        PartialPageUpdate.this.appendJavaScript(((OnEventHeaderItem) item).getCompleteJavaScript());
798                                        markItemRendered(item);
799                                }
800                        }
801                        else if (item instanceof OnDomReadyHeaderItem)
802                        {
803                                if (!wasItemRendered(item))
804                                {
805                                        PartialPageUpdate.this.domReadyJavaScripts.add(((OnDomReadyHeaderItem)item).getJavaScript());
806                                        markItemRendered(item);
807                                }
808                        }
809                        else if (headerRendering)
810                        {
811                                super.render(item);
812                        }
813                        else
814                        {
815                                LOG.debug("Only methods that can be called on IHeaderResponse outside renderHead() are #render(OnLoadHeaderItem) and #render(OnDomReadyHeaderItem)");
816                        }
817                }
818
819                @Override
820                protected Response getRealResponse()
821                {
822                        return RequestCycle.get().getResponse();
823                }
824        }
825
826        /**
827         * Wrapper of a response that buffers its contents.
828         *
829         * @author Igor Vaynberg (ivaynberg)
830         * @author Sven Meier (svenmeier)
831         * 
832         * @see ResponseBuffer#getContents()
833         * @see ResponseBuffer#reset()
834         */
835        protected static final class ResponseBuffer extends WebResponse
836        {
837                private final AppendingStringBuffer buffer = new AppendingStringBuffer(256);
838
839                private final WebResponse originalResponse;
840
841                /**
842                 * Constructor.
843                 *
844                 * @param originalResponse
845                 *      the original request cycle response
846                 */
847                private ResponseBuffer(WebResponse originalResponse)
848                {
849                        this.originalResponse = originalResponse;
850                }
851
852                /**
853                 * @see org.apache.wicket.request.Response#encodeURL(CharSequence)
854                 */
855                @Override
856                public String encodeURL(CharSequence url)
857                {
858                        return originalResponse.encodeURL(url);
859                }
860
861                /**
862                 * @return contents of the response
863                 */
864                public CharSequence getContents()
865                {
866                        return buffer;
867                }
868
869                /**
870                 * @see org.apache.wicket.request.Response#write(CharSequence)
871                 */
872                @Override
873                public void write(CharSequence cs)
874                {
875                        buffer.append(cs);
876                }
877
878                /**
879                 * Resets the response to a clean state so it can be reused to save on garbage.
880                 */
881                @Override
882                public void reset()
883                {
884                        buffer.clear();
885                }
886
887                @Override
888                public void write(byte[] array)
889                {
890                        throw new UnsupportedOperationException("Cannot write binary data.");
891                }
892
893                @Override
894                public void write(byte[] array, int offset, int length)
895                {
896                        throw new UnsupportedOperationException("Cannot write binary data.");
897                }
898
899                @Override
900                public Object getContainerResponse()
901                {
902                        return originalResponse.getContainerResponse();
903                }
904
905                @Override
906                public void addCookie(Cookie cookie)
907                {
908                        originalResponse.addCookie(cookie);
909                }
910
911                @Override
912                public void clearCookie(Cookie cookie)
913                {
914                        originalResponse.clearCookie(cookie);
915                }
916
917                @Override
918                public boolean isHeaderSupported()
919                {
920                        return originalResponse.isHeaderSupported();
921                }
922
923                @Override
924                public void setHeader(String name, String value)
925                {
926                        originalResponse.setHeader(name, value);
927                }
928
929                @Override
930                public void addHeader(String name, String value)
931                {
932                        originalResponse.addHeader(name, value);
933                }
934
935                @Override
936                public void setDateHeader(String name, Instant date)
937                {
938                        originalResponse.setDateHeader(name, date);
939                }
940
941                @Override
942                public void setContentLength(long length)
943                {
944                        originalResponse.setContentLength(length);
945                }
946
947                @Override
948                public void setContentType(String mimeType)
949                {
950                        originalResponse.setContentType(mimeType);
951                }
952
953                @Override
954                public void setStatus(int sc)
955                {
956                        originalResponse.setStatus(sc);
957                }
958
959                @Override
960                public void sendError(int sc, String msg)
961                {
962                        originalResponse.sendError(sc, msg);
963                }
964
965                @Override
966                public String encodeRedirectURL(CharSequence url)
967                {
968                        return originalResponse.encodeRedirectURL(url);
969                }
970
971                @Override
972                public void sendRedirect(String url)
973                {
974                        originalResponse.sendRedirect(url);
975                }
976
977                @Override
978                public boolean isRedirect()
979                {
980                        return originalResponse.isRedirect();
981                }
982
983                @Override
984                public void flush()
985                {
986                        originalResponse.flush();
987                }
988        }
989}