001/**
002 * Copyright (C) 2006-2020 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.server.front.memory;
017
018import static java.util.Collections.singletonList;
019
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.OutputStream;
023import java.io.PrintWriter;
024import java.nio.charset.StandardCharsets;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.List;
028import java.util.Locale;
029import java.util.Map;
030import java.util.TreeMap;
031import java.util.function.BiFunction;
032import java.util.function.BooleanSupplier;
033import java.util.function.Consumer;
034import java.util.function.Supplier;
035
036import javax.servlet.ServletOutputStream;
037import javax.servlet.WriteListener;
038import javax.servlet.http.Cookie;
039import javax.servlet.http.HttpServletResponse;
040
041public class InMemoryResponse implements HttpServletResponse {
042
043    private final BooleanSupplier isOpen;
044
045    private final Runnable onFlush;
046
047    private final Consumer<byte[]> writeCallback;
048
049    private final BiFunction<Integer, Map<String, List<String>>, String> preWrite;
050
051    private int code = HttpServletResponse.SC_OK;
052
053    private final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
054
055    private transient PrintWriter writer;
056
057    private transient ServletByteArrayOutputStream sosi;
058
059    private boolean commited = false;
060
061    private String encoding = "UTF-8";
062
063    private Locale locale = Locale.getDefault();
064
065    public InMemoryResponse(final BooleanSupplier isOpen, final Runnable onFlush, final Consumer<byte[]> write,
066            final BiFunction<Integer, Map<String, List<String>>, String> preWrite) {
067        this.isOpen = isOpen;
068        this.onFlush = onFlush;
069        this.writeCallback = write;
070        this.preWrite = preWrite;
071    }
072
073    /**
074     * sets a header to be sent back to the browser
075     *
076     * @param name
077     * the name of the header
078     * @param value
079     * the value of the header
080     */
081    public void setHeader(final String name, final String value) {
082        headers.put(name, new ArrayList<>(singletonList(value)));
083    }
084
085    @Override
086    public void setIntHeader(final String s, final int i) {
087        setHeader(s, Integer.toString(i));
088    }
089
090    @Override
091    public void setStatus(final int i) {
092        setCode(i);
093    }
094
095    @Override
096    public void setStatus(final int i, final String s) {
097        setCode(i);
098    }
099
100    @Override
101    public void addCookie(final Cookie cookie) {
102        setHeader(cookie.getName(), cookie.getValue());
103    }
104
105    @Override
106    public void addDateHeader(final String s, final long l) {
107        setHeader(s, Long.toString(l));
108    }
109
110    @Override
111    public void addHeader(final String s, final String s1) {
112        Collection<String> list = headers.get(s);
113        if (list == null) {
114            setHeader(s, s1);
115        } else {
116            list.add(s1);
117        }
118    }
119
120    @Override
121    public void addIntHeader(final String s, final int i) {
122        setIntHeader(s, i);
123    }
124
125    @Override
126    public boolean containsHeader(final String s) {
127        return headers.containsKey(s);
128    }
129
130    @Override
131    public String encodeURL(final String s) {
132        return toEncoded(s);
133    }
134
135    @Override
136    public String encodeRedirectURL(final String s) {
137        return toEncoded(s);
138    }
139
140    @Override
141    public String encodeUrl(final String s) {
142        return toEncoded(s);
143    }
144
145    @Override
146    public String encodeRedirectUrl(final String s) {
147        return encodeRedirectURL(s);
148    }
149
150    public String getHeader(final String name) {
151        final Collection<String> strings = headers.get(name);
152        return strings == null ? null : strings.iterator().next();
153    }
154
155    @Override
156    public Collection<String> getHeaderNames() {
157        return headers.keySet();
158    }
159
160    @Override
161    public Collection<String> getHeaders(final String s) {
162        return headers.get(s);
163    }
164
165    @Override
166    public int getStatus() {
167        return getCode();
168    }
169
170    @Override
171    public void sendError(final int i) throws IOException {
172        setCode(i);
173    }
174
175    @Override
176    public void sendError(final int i, final String s) throws IOException {
177        setCode(i);
178    }
179
180    @Override
181    public void sendRedirect(final String path) throws IOException {
182        if (commited) {
183            throw new IllegalStateException("response already committed");
184        }
185        resetBuffer();
186
187        try {
188            setStatus(SC_FOUND);
189
190            setHeader("Location", toEncoded(path));
191        } catch (final IllegalArgumentException e) {
192            setStatus(SC_NOT_FOUND);
193        }
194    }
195
196    @Override
197    public void setDateHeader(final String s, final long l) {
198        addDateHeader(s, l);
199    }
200
201    @Override
202    public ServletOutputStream getOutputStream() {
203        return sosi == null ? (sosi = createOutputStream()) : sosi;
204    }
205
206    @Override
207    public PrintWriter getWriter() {
208        return writer == null ? (writer = new PrintWriter(getOutputStream())) : writer;
209    }
210
211    @Override
212    public boolean isCommitted() {
213        return commited;
214    }
215
216    @Override
217    public void reset() {
218        createOutputStream();
219    }
220
221    private ServletByteArrayOutputStream createOutputStream() {
222        return sosi = new ServletByteArrayOutputStream(isOpen, onFlush, writeCallback,
223                () -> preWrite.apply(getStatus(), headers)) {
224
225            @Override
226            protected void beforeClose() throws IOException {
227                onClose(this);
228            }
229        };
230    }
231
232    public void flushBuffer() {
233        if (writer != null) {
234            writer.flush();
235        }
236    }
237
238    @Override
239    public int getBufferSize() {
240        return sosi.outputStream.size();
241    }
242
243    @Override
244    public String getCharacterEncoding() {
245        return encoding;
246    }
247
248    public void setCode(final int code) {
249        this.code = code;
250        commited = true;
251    }
252
253    public int getCode() {
254        return code;
255    }
256
257    public void setContentType(final String type) {
258        setHeader("Content-Type", type);
259    }
260
261    @Override
262    public void setLocale(final Locale loc) {
263        locale = loc;
264    }
265
266    public String getContentType() {
267        return getHeader("Content-Type");
268    }
269
270    @Override
271    public Locale getLocale() {
272        return locale;
273    }
274
275    @Override
276    public void resetBuffer() {
277        sosi.outputStream.reset();
278    }
279
280    @Override
281    public void setBufferSize(final int i) {
282        // no-op
283    }
284
285    @Override
286    public void setCharacterEncoding(final String s) {
287        encoding = s;
288    }
289
290    @Override
291    public void setContentLength(final int i) {
292        // no-op
293    }
294
295    @Override
296    public void setContentLengthLong(final long length) {
297        // no-op
298    }
299
300    private String toEncoded(final String url) {
301        return url;
302    }
303
304    protected void onClose(final OutputStream stream) throws IOException {
305        // no-op
306    }
307
308    private static class ServletByteArrayOutputStream extends ServletOutputStream {
309
310        private static final int BUFFER_SIZE = 1024 * 8;
311
312        private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
313
314        private final BooleanSupplier isOpen;
315
316        private final Runnable onFlush;
317
318        private final Consumer<byte[]> writer;
319
320        private final Supplier<String> preWrite;
321
322        private boolean closed;
323
324        private boolean headerWritten;
325
326        private ServletByteArrayOutputStream(final BooleanSupplier isOpen, final Runnable onFlush,
327                final Consumer<byte[]> write, final Supplier<String> preWrite) {
328            this.isOpen = isOpen;
329            this.onFlush = onFlush;
330            this.writer = write;
331            this.preWrite = preWrite;
332        }
333
334        @Override
335        public boolean isReady() {
336            return true;
337        }
338
339        @Override
340        public void setWriteListener(final WriteListener listener) {
341            // no-op
342        }
343
344        @Override
345        public void write(final int b) throws IOException {
346            outputStream.write(b);
347        }
348
349        @Override
350        public void write(final byte[] b, final int off, final int len) {
351            outputStream.write(b, off, len);
352        }
353
354        public void writeTo(final OutputStream out) throws IOException {
355            outputStream.writeTo(out);
356        }
357
358        public void reset() {
359            outputStream.reset();
360        }
361
362        @Override
363        public void flush() throws IOException {
364            if (!isOpen.getAsBoolean()) {
365                return;
366            }
367            if (outputStream.size() >= BUFFER_SIZE) {
368                doFlush();
369            }
370        }
371
372        @Override
373        public void close() throws IOException {
374            if (closed) {
375                return;
376            }
377
378            beforeClose();
379            doFlush();
380            closed = true;
381        }
382
383        protected void beforeClose() throws IOException {
384            // no-op
385        }
386
387        private void doFlush() {
388            final byte[] array = outputStream.toByteArray();
389            final boolean written = array.length > 0 || !headerWritten;
390
391            if (!headerWritten) {
392                final String headers = preWrite.get();
393                if (!headers.isEmpty()) {
394                    writer.accept(headers.getBytes(StandardCharsets.UTF_8));
395                }
396                headerWritten = true;
397            }
398
399            if (array.length > 0) {
400                outputStream.reset();
401                writer.accept(array);
402            }
403
404            if (written) {
405                onFlush.run();
406            }
407        }
408    }
409}