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}