001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.protocol.http;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.time.Instant;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026import java.util.function.Consumer;
027import jakarta.servlet.http.Cookie;
028import org.apache.wicket.Application;
029import org.apache.wicket.WicketRuntimeException;
030import org.apache.wicket.request.Response;
031import org.apache.wicket.request.http.WebResponse;
032import org.apache.wicket.response.filter.IResponseFilter;
033import org.apache.wicket.util.lang.Args;
034import org.apache.wicket.util.string.AppendingStringBuffer;
035
036/**
037 * Subclass of {@link WebResponse} that buffers the actions and performs those on another response.
038 * 
039 * @see #writeTo(WebResponse)
040 * 
041 * @author Matej Knopp
042 */
043public class BufferedWebResponse extends WebResponse implements IMetaDataBufferingWebResponse
044{
045        private final WebResponse originalResponse;
046
047        /**
048         * Construct.
049         * 
050         * @param originalResponse
051         */
052        public BufferedWebResponse(WebResponse originalResponse)
053        {
054                // if original response had some metadata set
055                // we should transfer it to the current response
056                if (originalResponse instanceof IMetaDataBufferingWebResponse)
057                {
058                        ((IMetaDataBufferingWebResponse)originalResponse).writeMetaData(this);
059                }
060                this.originalResponse = originalResponse;
061        }
062
063        /**
064         * transfer cookie operations (add, clear) to given web response
065         * 
066         * @param response
067         *            web response that should receive the current cookie operation
068         */
069        @Override
070        public void writeMetaData(WebResponse response)
071        {
072                for (Action action : actions)
073                {
074                        if (action.getType() == ActionType.HEADER)
075                                action.invoke(response);
076                }
077        }
078
079
080        @Override
081        public String encodeURL(CharSequence url)
082        {
083                if (originalResponse != null)
084                {
085                        return originalResponse.encodeURL(url);
086                }
087                else
088                {
089                        return url != null ? url.toString() : null;
090                }
091        }
092
093        @Override
094        public String encodeRedirectURL(CharSequence url)
095        {
096                if (originalResponse != null)
097                {
098                        return originalResponse.encodeRedirectURL(url);
099                }
100                else
101                {
102                        return url != null ? url.toString() : null;
103                }
104        }
105
106        private enum ActionType {
107                /**
108                 * Actions not related directly to the content of the response, eg setting cookies, headers.
109                 */
110                HEADER,
111                REDIRECT,
112                NORMAL,
113                /**
114                 * Actions directly related to the data of the response, eg writing output, etc.
115                 */
116                DATA;
117
118                protected final Action action(Consumer<WebResponse> action) {
119                        return new Action(this, action);
120                }
121        }
122
123        private static final class Action implements Comparable<Action>
124        {
125                private final ActionType type;
126                private final Consumer<WebResponse> action;
127
128                private Action(ActionType type, Consumer<WebResponse> action)
129                {
130                        this.type = type;
131                        this.action = action;
132                }
133
134                protected final void invoke(WebResponse response)
135                {
136                        action.accept(response);
137                }
138
139                protected final ActionType getType()
140                {
141                        return type;
142                }
143
144                @Override
145                public int compareTo(Action o)
146                {
147                        return getType().ordinal() - o.getType().ordinal();
148                }
149        }
150
151        private final List<Action> actions = new ArrayList<Action>();
152        private StringBuilder charSequenceBuilder;
153        private ByteArrayOutputStream dataStream;
154
155        @Override
156        public void reset()
157        {
158                super.reset();
159                actions.clear();
160                charSequenceBuilder = null;
161                dataStream = null;
162        }
163
164        @Override
165        public void addCookie(Cookie cookie)
166        {
167                actions.add(ActionType.HEADER.action(res -> res.addCookie(cookie)));
168        }
169
170        @Override
171        public void clearCookie(Cookie cookie)
172        {
173                actions.add(ActionType.HEADER.action(res -> res.clearCookie(cookie)));
174        }
175
176        @Override
177        public void setContentLength(long length)
178        {
179                actions.add(ActionType.HEADER.action(res -> res.setContentLength(length)));
180        }
181
182        @Override
183        public void setContentType(String mimeType)
184        {
185                actions.add(ActionType.HEADER.action(res -> res.setContentType(mimeType)));
186        }
187
188        @Override
189        public void setDateHeader(String name, Instant date)
190        {
191                Args.notNull(date, "date");
192                actions.add(ActionType.HEADER.action(res -> res.setDateHeader(name, date)));
193        }
194
195        @Override
196        public boolean isHeaderSupported()
197        {
198                return originalResponse.isHeaderSupported();
199        }
200
201        @Override
202        public void setHeader(String name, String value)
203        {
204                actions.add(ActionType.HEADER.action(res -> res.setHeader(name, value)));
205        }
206
207        @Override
208        public void addHeader(String name, String value)
209        {
210                actions.add(ActionType.HEADER.action(res -> res.addHeader(name, value)));
211        }
212
213        @Override
214        public void disableCaching()
215        {
216                actions.add(ActionType.HEADER.action(WebResponse::disableCaching));
217        }
218
219        @Override
220        public void write(CharSequence sequence)
221        {
222                if (dataStream != null)
223                {
224                        throw new IllegalStateException(
225                                "Can't call write(CharSequence) after write(byte[]) has been called.");
226                }
227
228                if (charSequenceBuilder == null)
229                {
230                        StringBuilder builder = new StringBuilder(4096);
231                        charSequenceBuilder = builder;
232                        actions.add(ActionType.DATA.action(res ->
233                        {
234                                AppendingStringBuffer responseBuffer = new AppendingStringBuffer(builder);
235
236                                List<IResponseFilter> responseFilters = Application.get()
237                                                .getRequestCycleSettings()
238                                                .getResponseFilters();
239
240                                if (responseFilters != null)
241                                {
242                                        for (IResponseFilter filter : responseFilters)
243                                        {
244                                                responseBuffer = filter.filter(responseBuffer);
245                                        }
246                                }
247                                res.write(responseBuffer);
248                        }));
249                }
250                charSequenceBuilder.append(sequence);
251        }
252
253        /**
254         * Returns the text already written to this response.
255         * 
256         * @return text
257         */
258        public CharSequence getText()
259        {
260                if (dataStream != null)
261                {
262                        throw new IllegalStateException("write(byte[]) has already been called.");
263                }
264                if (charSequenceBuilder != null)
265                {
266                        return charSequenceBuilder;
267                }
268                else
269                {
270                        return null;
271                }
272        }
273
274        /**
275         * Replaces the text in this response
276         * 
277         * @param text
278         */
279        public void setText(CharSequence text)
280        {
281                if (dataStream != null)
282                {
283                        throw new IllegalStateException("write(byte[]) has already been called.");
284                }
285                if (charSequenceBuilder != null)
286                {
287                        charSequenceBuilder.setLength(0);
288                }
289                write(text);
290        }
291
292        @Override
293        public void write(byte[] array)
294        {
295                write(array, 0, array.length);
296        }
297
298        @Override
299        public void write(byte[] array, int offset, int length)
300        {
301                if (charSequenceBuilder != null)
302                {
303                        throw new IllegalStateException(
304                                "Can't call write(byte[]) after write(CharSequence) has been called.");
305                }
306                if (dataStream == null)
307                {
308                        ByteArrayOutputStream stream = new ByteArrayOutputStream();
309                        dataStream = stream;
310                        actions.add(ActionType.DATA.action(res -> writeStream(res, stream)));
311                }
312                dataStream.write(array, offset, length);
313        }
314
315        @Override
316        public void sendRedirect(String url)
317        {
318                actions.add(ActionType.REDIRECT.action(res -> res.sendRedirect(url)));
319        }
320
321        @Override
322        public void setStatus(int sc)
323        {
324                actions.add(ActionType.HEADER.action(res -> res.setStatus(sc)));
325        }
326
327        @Override
328        public void sendError(int sc, String msg)
329        {
330                actions.add(ActionType.NORMAL.action(res -> res.sendError(sc, msg)));
331        }
332
333        /**
334         * Writes the content of the buffer to the specified response. Also sets the properties and and
335         * headers.
336         * 
337         * @param response
338         */
339        public void writeTo(final WebResponse response)
340        {
341                Args.notNull(response, "response");
342
343                Collections.sort(actions);
344
345                for (Action action : actions)
346                {
347                        action.invoke(response);
348                }
349        }
350
351        @Override
352        public boolean isRedirect()
353        {
354                for (Action action : actions)
355                {
356                        if (action.getType() == ActionType.REDIRECT)
357                        {
358                                return true;
359                        }
360                }
361                return false;
362        }
363
364        @Override
365        public void flush()
366        {
367                actions.add(ActionType.NORMAL.action(WebResponse::flush));
368        }
369
370        private static void writeStream(final Response response, ByteArrayOutputStream stream)
371        {
372                final boolean copied[] = { false };
373                try
374                {
375                        // try to avoid copying the array
376                        stream.writeTo(new OutputStream()
377                        {
378                                @Override
379                                public void write(int b) throws IOException
380                                {
381
382                                }
383
384                                @Override
385                                public void write(byte[] b, int off, int len) throws IOException
386                                {
387                                        if (off == 0 && len == b.length)
388                                        {
389                                                response.write(b);
390                                                copied[0] = true;
391                                        }
392                                }
393                        });
394                }
395                catch (IOException e1)
396                {
397                        throw new WicketRuntimeException(e1);
398                }
399                if (copied[0] == false)
400                {
401                        response.write(stream.toByteArray());
402                }
403        }
404
405        /**
406         * @see java.lang.Object#toString()
407         */
408        @Override
409        public String toString()
410        {
411                final String toString;
412                if (charSequenceBuilder != null)
413                {
414                        toString = charSequenceBuilder.toString();
415                }
416                else
417                {
418                        toString = super.toString();
419                }
420                return toString;
421        }
422
423        @Override
424        public Object getContainerResponse()
425        {
426                return originalResponse.getContainerResponse();
427        }
428}