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.ws; 018 019import org.apache.wicket.Application; 020import org.apache.wicket.MetaDataKey; 021import org.apache.wicket.Page; 022import org.apache.wicket.protocol.ws.api.IWebSocketConnection; 023import org.apache.wicket.protocol.ws.api.IWebSocketConnectionFilter; 024import org.apache.wicket.protocol.ws.api.IWebSocketSession; 025import org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer; 026import org.apache.wicket.protocol.ws.api.ServletRequestCopy; 027import org.apache.wicket.protocol.ws.api.WebSocketConnectionFilterCollection; 028import org.apache.wicket.protocol.ws.api.WebSocketRequest; 029import org.apache.wicket.protocol.ws.api.WebSocketRequestHandler; 030import org.apache.wicket.protocol.ws.api.WebSocketResponse; 031import org.apache.wicket.protocol.ws.api.registry.IWebSocketConnectionRegistry; 032import org.apache.wicket.protocol.ws.api.registry.SimpleWebSocketConnectionRegistry; 033import org.apache.wicket.protocol.ws.concurrent.Executor; 034import org.apache.wicket.request.Url; 035import org.apache.wicket.request.cycle.RequestCycle; 036import org.apache.wicket.request.http.WebRequest; 037import org.apache.wicket.request.http.WebResponse; 038import org.apache.wicket.util.lang.Args; 039import org.apache.wicket.util.string.Strings; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import javax.servlet.http.HttpServletRequest; 044import java.util.concurrent.SynchronousQueue; 045import java.util.concurrent.ThreadPoolExecutor; 046import java.util.concurrent.TimeUnit; 047import java.util.concurrent.atomic.AtomicInteger; 048import java.util.concurrent.atomic.AtomicReference; 049import java.util.function.Function; 050 051/** 052 * Web Socket related settings. 053 * 054 * More documentation is available about each setting in the setter method for the property. 055 */ 056public class WebSocketSettings 057{ 058 private static final Logger LOG = LoggerFactory.getLogger(WebSocketSettings.class); 059 060 private static final MetaDataKey<WebSocketSettings> KEY = new MetaDataKey<>() 061 { 062 }; 063 064 /** 065 * A flag indicating whether JavaxWebSocketFilter is in use. 066 * When using JSR356 based implementations the ws:// url should not 067 * use the WicketFilter's filterPath because JSR356 Upgrade connections 068 * are never passed to the Servlet Filters. 069 */ 070 private static boolean USING_JAVAX_WEB_SOCKET = false; 071 072 static 073 { 074 try 075 { 076 Class.forName("org.apache.wicket.protocol.ws.javax.JavaxWebSocketFilter"); 077 USING_JAVAX_WEB_SOCKET = true; 078 LOG.debug("Using JSR356 Native WebSocket implementation!"); 079 } catch (ClassNotFoundException e) 080 { 081 LOG.debug("Using non-JSR356 Native WebSocket implementation!"); 082 } 083 } 084 085 private final AtomicReference<CharSequence> filterPrefix = new AtomicReference<>(); 086 private final AtomicReference<CharSequence> contextPath = new AtomicReference<>(); 087 private final AtomicReference<CharSequence> baseUrl = new AtomicReference<>(); 088 private final AtomicInteger port = new AtomicInteger(); 089 private final AtomicInteger securePort = new AtomicInteger(); 090 091 /** 092 * Holds this WebSocketSettings in the Application's metadata. 093 * This way wicket-core module doesn't have reference to wicket-native-websocket. 094 */ 095 public static final class Holder 096 { 097 public static WebSocketSettings get(Application application) 098 { 099 WebSocketSettings settings = application.getMetaData(KEY); 100 if (settings == null) 101 { 102 synchronized (application) 103 { 104 settings = application.getMetaData(KEY); 105 if (settings == null) 106 { 107 settings = new WebSocketSettings(); 108 set(application, settings); 109 } 110 } 111 } 112 return settings; 113 } 114 115 public static void set(Application application, WebSocketSettings settings) 116 { 117 application.setMetaData(KEY, settings); 118 } 119 } 120 121 /** 122 * The executor that handles the processing of Web Socket push message broadcasts. 123 */ 124 private Executor webSocketPushMessageExecutor = new WebSocketPushMessageExecutor(); 125 126 /** 127 * The executor that handles broadcast of the {@link org.apache.wicket.protocol.ws.api.event.WebSocketPayload} 128 * via Wicket's event bus. 129 */ 130 private Executor sendPayloadExecutor = new SameThreadExecutor(); 131 132 /** 133 * Tracks all currently connected WebSocket clients 134 */ 135 private IWebSocketConnectionRegistry connectionRegistry = new SimpleWebSocketConnectionRegistry(); 136 137 /** 138 * A filter that may reject an incoming connection 139 */ 140 private IWebSocketConnectionFilter connectionFilter; 141 142 /** 143 * A {@link org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer} that allows to configure 144 * {@link org.apache.wicket.protocol.ws.api.IWebSocketSession}s. 145 */ 146 private IWebSocketSessionConfigurer socketSessionConfigurer = new IWebSocketSessionConfigurer() { 147 @Override 148 public void configureSession(IWebSocketSession webSocketSession) { 149 // does nothing by default 150 } 151 }; 152 153 /** 154 * A function that decides whether to notify the page/resource on 155 * web socket connection closed event. 156 * The page notification leads to deserialization of the page instance from 157 * the page store and sometimes this is not wanted. 158 */ 159 private Function<Integer, Boolean> notifyOnCloseEvent = (code) -> true; 160 161 /** 162 * Flag that allows to use asynchronous push. By default, it is set to <code>false</code>. 163 */ 164 private boolean asynchronousPush = false; 165 166 /** 167 * The timeout to use for asynchronous push. By default, it is -1 which means use timeout configured by 168 * server implementation. 169 */ 170 private long asynchronousPushTimeout = -1; 171 172 public boolean shouldNotifyOnCloseEvent(int closeCode) 173 { 174 return notifyOnCloseEvent == null || notifyOnCloseEvent.apply(closeCode); 175 } 176 177 public void setNotifyOnCloseEvent(Function<Integer, Boolean> notifyOnCloseEvent) 178 { 179 this.notifyOnCloseEvent = notifyOnCloseEvent; 180 } 181 182 /** 183 * A function that decides whether to notify the page/resource on 184 * web socket error event. 185 * The page notification leads to deserialization of the page instance from 186 * the page store and sometimes this is not wanted. 187 */ 188 private Function<Throwable, Boolean> notifyOnErrorEvent = (throwable) -> true; 189 190 public boolean shouldNotifyOnErrorEvent(Throwable throwable) 191 { 192 return notifyOnErrorEvent == null || notifyOnErrorEvent.apply(throwable); 193 } 194 195 public void setNotifyOnErrorEvent(Function<Throwable, Boolean> notifyOnErrorEvent) 196 { 197 this.notifyOnErrorEvent = notifyOnErrorEvent; 198 } 199 200 /** 201 * Set the executor for processing websocket push messages broadcasted to all sessions. 202 * Default executor does all the processing in the caller thread. Using a proper thread pool is adviced 203 * for applications that send push events from ajax calls to avoid page level deadlocks. 204 * 205 * @param executor 206 * The executor used for processing push messages. 207 */ 208 public WebSocketSettings setWebSocketPushMessageExecutor(Executor executor) 209 { 210 Args.notNull(executor, "executor"); 211 this.webSocketPushMessageExecutor = executor; 212 return this; 213 } 214 215 /** 216 * @return the executor for processing websocket push messages broadcasted to all sessions. 217 */ 218 public Executor getWebSocketPushMessageExecutor() 219 { 220 return webSocketPushMessageExecutor; 221 } 222 223 /** 224 * @return The registry that tracks all currently connected WebSocket clients 225 */ 226 public IWebSocketConnectionRegistry getConnectionRegistry() 227 { 228 return connectionRegistry; 229 } 230 231 /** 232 * Sets the connection registry 233 * 234 * @param connectionRegistry 235 * The registry that tracks all currently connected WebSocket clients 236 * @return {@code this}, for method chaining 237 */ 238 public WebSocketSettings setConnectionRegistry(IWebSocketConnectionRegistry connectionRegistry) 239 { 240 Args.notNull(connectionRegistry, "connectionRegistry"); 241 this.connectionRegistry = connectionRegistry; 242 return this; 243 } 244 245 /** 246 * The executor that broadcasts the {@link org.apache.wicket.protocol.ws.api.event.WebSocketPayload} 247 * via Wicket's event bus. 248 * Default executor does all the processing in the caller thread. 249 * 250 * @param sendPayloadExecutor 251 * The executor used for broadcasting the events with web socket payloads to 252 * {@link org.apache.wicket.protocol.ws.api.WebSocketBehavior}s and 253 * {@link org.apache.wicket.protocol.ws.api.WebSocketResource}s. 254 */ 255 public WebSocketSettings setSendPayloadExecutor(Executor sendPayloadExecutor) 256 { 257 Args.notNull(sendPayloadExecutor, "sendPayloadExecutor"); 258 this.sendPayloadExecutor = sendPayloadExecutor; 259 return this; 260 } 261 262 /** 263 * The executor that broadcasts the {@link org.apache.wicket.protocol.ws.api.event.WebSocketPayload} 264 * via Wicket's event bus. 265 * 266 * @return 267 * The executor used for broadcasting the events with web socket payloads to 268 * {@link org.apache.wicket.protocol.ws.api.WebSocketBehavior}s and 269 * {@link org.apache.wicket.protocol.ws.api.WebSocketResource}s. 270 */ 271 public Executor getSendPayloadExecutor() 272 { 273 return sendPayloadExecutor; 274 } 275 276 /** 277 * Sets the IWebSocketSessionConfigurer 278 * @param socketSessionConfigurer A non-null {@link org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer} 279 */ 280 public void setSocketSessionConfigurer(IWebSocketSessionConfigurer socketSessionConfigurer) { 281 Args.notNull(socketSessionConfigurer, "socketSessionConfigurer"); 282 this.socketSessionConfigurer = socketSessionConfigurer; 283 } 284 285 /** 286 * @return returns the {@link org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer} 287 */ 288 public IWebSocketSessionConfigurer getSocketSessionConfigurer() { 289 return socketSessionConfigurer; 290 } 291 292 /** 293 * Sets the filter for checking the incoming connections 294 * 295 * @param connectionFilter 296 * the filter for checking the incoming connections 297 * @see WebSocketConnectionFilterCollection 298 */ 299 public void setConnectionFilter(IWebSocketConnectionFilter connectionFilter) 300 { 301 this.connectionFilter = connectionFilter; 302 } 303 304 /** 305 * @return the filter for checking the incoming connections 306 * @see WebSocketConnectionFilterCollection 307 */ 308 public IWebSocketConnectionFilter getConnectionFilter() 309 { 310 return this.connectionFilter; 311 } 312 313 /** 314 * A factory method for the {@link org.apache.wicket.request.http.WebResponse} 315 * that should be used to write the response back to the client/browser 316 * 317 * @param connection 318 * The active web socket connection 319 * @return the response object that should be used to write the response back to the client 320 */ 321 public WebResponse newWebSocketResponse(IWebSocketConnection connection) 322 { 323 return newWebSocketResponse(connection, isAsynchronousPush(), getAsynchronousPushTimeout()); 324 } 325 326 /** 327 * A factory method for the {@link org.apache.wicket.request.http.WebResponse} 328 * that should be used to write the response back to the client/browser 329 * 330 * @param connection 331 * The active web socket connection 332 * @param asynchronousPush 333 * Whether asynchronous push is wanted or not. 334 * @param timeout 335 * The timeout to be used for push operations 336 * @return the response object that should be used to write the response back to the client 337 */ 338 public WebResponse newWebSocketResponse(IWebSocketConnection connection, boolean asynchronousPush, long timeout) 339 { 340 return new WebSocketResponse(connection, asynchronousPush, timeout); 341 } 342 343 /** 344 * A factory method for creating instances of {@link org.apache.wicket.protocol.ws.api.WebSocketRequestHandler} 345 * for processing a web socket request 346 * 347 * @param page 348 * The page with the web socket client. A dummy page in case of usage of 349 * {@link org.apache.wicket.protocol.ws.api.WebSocketResource} 350 * @param connection 351 * The active web socket connection 352 * @return a new instance of WebSocketRequestHandler for processing a web socket request 353 */ 354 public WebSocketRequestHandler newWebSocketRequestHandler(Page page, IWebSocketConnection connection) 355 { 356 return new WebSocketRequestHandler(page, connection); 357 } 358 359 /** 360 * A factory method for the {@link org.apache.wicket.request.http.WebRequest} 361 * that should be used in the WebSocket processing request cycle 362 * 363 * @param request 364 * The upgraded http request 365 * @param filterPath 366 * The configured filter path of WicketFilter in web.xml 367 * @return the request object that should be used in the WebSocket processing request cycle 368 */ 369 public WebRequest newWebSocketRequest(HttpServletRequest request, String filterPath) 370 { 371 return new WebSocketRequest(new ServletRequestCopy(request), filterPath); 372 } 373 374 public void setFilterPrefix(final CharSequence filterPrefix) 375 { 376 this.filterPrefix.set(filterPrefix); 377 } 378 379 public CharSequence getFilterPrefix() 380 { 381 if (filterPrefix.get() == null) 382 { 383 if (USING_JAVAX_WEB_SOCKET) 384 { 385 filterPrefix.compareAndSet(null, ""); 386 } 387 else 388 { 389 filterPrefix.compareAndSet(null, RequestCycle.get().getRequest().getFilterPath()); 390 } 391 } 392 return filterPrefix.get(); 393 } 394 395 public void setContextPath(final CharSequence contextPath) 396 { 397 this.contextPath.set(contextPath); 398 } 399 400 public CharSequence getContextPath() 401 { 402 contextPath.compareAndSet(null, RequestCycle.get().getRequest().getContextPath()); 403 return contextPath.get(); 404 } 405 406 public void setBaseUrl(final CharSequence baseUrl) 407 { 408 this.baseUrl.set(baseUrl); 409 } 410 411 public CharSequence getBaseUrl() 412 { 413 if (baseUrl.get() == null) 414 { 415 Url _baseUrl = RequestCycle.get().getUrlRenderer().getBaseUrl(); 416 return Strings.escapeMarkup(_baseUrl.toString()); 417 } 418 return baseUrl.get(); 419 } 420 421 /** 422 * Sets the port that should be used for <code>ws:</code> connections. 423 * If unset then the current HTTP port will be used. 424 * 425 * @param wsPort The custom port for WS connections 426 */ 427 public void setPort(int wsPort) 428 { 429 this.port.set(wsPort); 430 } 431 432 /** 433 * @return The custom port for WS connections 434 */ 435 public Integer getPort() 436 { 437 return port.get(); 438 } 439 440 /** 441 * Sets the port that should be used for <code>wss:</code> connections. 442 * If unset then the current HTTPS port will be used. 443 * 444 * @param wssPort The custom port for WSS connections 445 */ 446 public void setSecurePort(int wssPort) 447 { 448 this.securePort.set(wssPort); 449 } 450 451 /** 452 * @return The custom port for WSS connections 453 */ 454 public Integer getSecurePort() 455 { 456 return securePort.get(); 457 } 458 459 /** 460 * Simple executor that runs the tasks in the caller thread. 461 */ 462 public static class SameThreadExecutor implements Executor 463 { 464 @Override 465 public void run(Runnable command) 466 { 467 command.run(); 468 } 469 } 470 471 public static class WebSocketPushMessageExecutor implements Executor 472 { 473 /** 474 * An executor that should be used when the WebSocket message is pushed 475 * from non-http worker thread. 476 */ 477 private final java.util.concurrent.Executor nonHttpRequestExecutor; 478 479 /** 480 * An executor that is used when the WebSocket push is initiated in 481 * http worker thread. In this case the WebSocket processing should be 482 * off-loaded to a different thread that should wait for the page instance 483 * lock. 484 */ 485 private final java.util.concurrent.Executor httpRequestExecutor; 486 487 /** 488 * For non-http worker threads pushes the WebSocket runnable in the same request. 489 * For http worker threads uses an elastic thread pool of 1-8 threads. 490 * 491 * Use {@link WebSocketPushMessageExecutor#WebSocketPushMessageExecutor(java.util.concurrent.Executor, java.util.concurrent.Executor)} 492 * for custom behavior and/or settings 493 */ 494 public WebSocketPushMessageExecutor() 495 { 496 this(Runnable::run, new ThreadPoolExecutor(1, 8, 497 60L, TimeUnit.SECONDS, 498 new SynchronousQueue<>(), 499 new ThreadFactory())); 500 } 501 502 public WebSocketPushMessageExecutor(java.util.concurrent.Executor nonHttpRequestExecutor, java.util.concurrent.Executor httpRequestExecutor) 503 { 504 this.nonHttpRequestExecutor = nonHttpRequestExecutor; 505 this.httpRequestExecutor = httpRequestExecutor; 506 } 507 508 @Override 509 public void run(final Runnable command) 510 { 511 if (RequestCycle.get() != null) 512 { 513 httpRequestExecutor.execute(command); 514 } 515 else 516 { 517 nonHttpRequestExecutor.execute(command); 518 } 519 } 520 } 521 522 public static class ThreadFactory implements java.util.concurrent.ThreadFactory 523 { 524 private final AtomicInteger counter = new AtomicInteger(); 525 526 @Override 527 public Thread newThread(final Runnable r) 528 { 529 return new Thread(r, "Wicket-WebSocket-HttpRequest-Thread-" + counter.getAndIncrement()); 530 } 531 } 532 533 public void setAsynchronousPush(boolean asynchronousPush) 534 { 535 this.asynchronousPush = asynchronousPush; 536 } 537 538 public boolean isAsynchronousPush() 539 { 540 return asynchronousPush; 541 } 542 543 public void setAsynchronousPushTimeout(long asynchronousPushTimeout) 544 { 545 this.asynchronousPushTimeout = asynchronousPushTimeout; 546 } 547 548 public long getAsynchronousPushTimeout() 549 { 550 return asynchronousPushTimeout; 551 } 552}