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.api; 018 019import java.io.IOException; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.concurrent.CompletableFuture; 023import java.util.concurrent.Future; 024 025import org.apache.wicket.Component; 026import org.apache.wicket.core.request.handler.AbstractPartialPageRequestHandler; 027import org.apache.wicket.core.request.handler.logger.PageLogData; 028import org.apache.wicket.page.PartialPageUpdate; 029import org.apache.wicket.page.XmlPartialPageUpdate; 030import org.apache.wicket.request.ILogData; 031import org.apache.wicket.request.IRequestCycle; 032import org.apache.wicket.response.StringResponse; 033import org.apache.wicket.util.lang.Args; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037/** 038 * A handler of WebSocket requests. 039 * 040 * @since 6.0 041 */ 042public class WebSocketRequestHandler extends AbstractPartialPageRequestHandler implements IWebSocketRequestHandler 043{ 044 private static final Logger LOG = LoggerFactory.getLogger(WebSocketRequestHandler.class); 045 046 047 private final IWebSocketConnection connection; 048 049 private PartialPageUpdate update; 050 051 private PageLogData logData; 052 053 public WebSocketRequestHandler(final Component component, final IWebSocketConnection connection) 054 { 055 super(Args.notNull(component, "component").getPage()); 056 this.connection = Args.notNull(connection, "connection"); 057 } 058 059 @Override 060 public void push(CharSequence message) 061 { 062 if (connection.isOpen()) 063 { 064 Args.notNull(message, "message"); 065 try 066 { 067 connection.sendMessage(message.toString()); 068 } catch (IOException iox) 069 { 070 LOG.error("An error occurred while pushing text message.", iox); 071 } 072 } 073 else 074 { 075 LOG.warn("The websocket connection is already closed. Cannot push the text message '{}'", message); 076 } 077 } 078 079 @Override 080 public Future<Void> pushAsync(CharSequence message, long timeout) 081 { 082 if (connection.isOpen()) 083 { 084 Args.notNull(message, "message"); 085 return connection.sendMessageAsync(message.toString(), timeout); 086 } 087 else 088 { 089 LOG.warn("The websocket connection is already closed. Cannot push the text message '{}'", message); 090 } 091 return CompletableFuture.completedFuture(null); 092 } 093 094 @Override 095 public Future<Void> pushAsync(CharSequence message) 096 { 097 return pushAsync(message, -1); 098 } 099 100 @Override 101 public void push(byte[] message, int offset, int length) 102 { 103 if (connection.isOpen()) 104 { 105 Args.notNull(message, "message"); 106 try 107 { 108 connection.sendMessage(message, offset, length); 109 } catch (IOException iox) 110 { 111 LOG.error("An error occurred while pushing binary message.", iox); 112 } 113 } 114 else 115 { 116 LOG.warn("The websocket connection is already closed. Cannot push the binary message '{}'", message); 117 } 118 } 119 120 @Override 121 public Future<Void> pushAsync(byte[] message, int offset, int length) 122 { 123 return pushAsync(message, offset, length, -1); 124 } 125 126 @Override 127 public Future<Void> pushAsync(byte[] message, int offset, int length, long timeout) 128 { 129 if (connection.isOpen()) 130 { 131 Args.notNull(message, "message"); 132 return connection.sendMessageAsync(message, offset, length, timeout); 133 } 134 else 135 { 136 LOG.warn("The websocket connection is already closed. Cannot push the binary message '{}'", message); 137 } 138 return CompletableFuture.completedFuture(null); 139 } 140 141 /** 142 * @return if <code>true</code> then EMPTY partial updates will se send. If <code>false</code> then EMPTY 143 * partial updates will be skipped. A possible use case is: a page receives and a push event but no one is 144 * listening to it, and nothing is added to {@link org.apache.wicket.protocol.ws.api.WebSocketRequestHandler} 145 * thus no real push to client is needed. For compatibilities this is set to true. Thus EMPTY updates are sent 146 * by default. 147 */ 148 protected boolean shouldPushWhenEmpty() 149 { 150 return true; 151 } 152 153 protected PartialPageUpdate getUpdate() { 154 if (update == null) { 155 update = new XmlPartialPageUpdate(getPage()); 156 } 157 return update; 158 } 159 160 161 @Override 162 public Collection<? extends Component> getComponents() 163 { 164 if (update == null) { 165 return Collections.emptyList(); 166 } else { 167 return update.getComponents(); 168 } 169 } 170 171 @Override 172 public ILogData getLogData() 173 { 174 return logData; 175 } 176 177 178 @Override 179 public void respond(IRequestCycle requestCycle) 180 { 181 if (update != null && (shouldPushWhenEmpty() || !update.isEmpty())) { 182 // see WICKET-7098 183 // A malformed XML is generated if a runtime exception happen during rendering phase of a web 184 // socket push request. Writing to a buffer allows to generate a proper XML 185 // as request's buffer will not be polluted by partial write operations 186 StringResponse bodyResponse = new StringResponse(); 187 // additionally, we use the charset for the request instead of a hardcoded UTF-8 188 update.writeTo(bodyResponse, requestCycle.getRequest().getCharset().name()); 189 requestCycle.getResponse().write(bodyResponse.getBuffer()); 190 } 191 } 192 193 @Override 194 public void detach(IRequestCycle requestCycle) 195 { 196 if (logData == null) 197 { 198 logData = new PageLogData(getPage()); 199 } 200 201 if (update != null) { 202 update.detach(requestCycle); 203 update = null; 204 } 205 } 206}