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 javax.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}