/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.websocket;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.impl.DefaultProducer;
import org.apache.camel.util.StopWatch;
public class WebsocketProducer extends DefaultProducer implements WebsocketProducerConsumer {
private WebsocketStore store;
private final Boolean sendToAll;
private final WebsocketEndpoint endpoint;
public WebsocketProducer(WebsocketEndpoint endpoint) {
super(endpoint);
this.sendToAll = endpoint.getSendToAll();
this.endpoint = endpoint;
}
@Override
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn();
Object message = in.getMandatoryBody();
if (!(message == null || message instanceof String || message instanceof byte[])) {
message = in.getMandatoryBody(String.class);
}
if (isSendToAllSet(in)) {
sendToAll(store, message, exchange);
} else {
// look for connection key and get Websocket
String connectionKey = in.getHeader(WebsocketConstants.CONNECTION_KEY, String.class);
if (connectionKey != null) {
String pathSpec = "";
if (endpoint.getResourceUri() != null) {
pathSpec = WebsocketComponent.createPathSpec(endpoint.getResourceUri());
}
DefaultWebsocket websocket = store.get(connectionKey + pathSpec);
log.debug("Sending to connection key {} -> {}", connectionKey, message);
Future<Void> future = sendMessage(websocket, message);
if (future != null) {
int timeout = endpoint.getSendTimeout();
future.get(timeout, TimeUnit.MILLISECONDS);
if (!future.isCancelled() && !future.isDone()) {
throw new WebsocketSendException("Failed to send message to the connection within " + timeout + " millis.", exchange);
}
}
} else {
throw new WebsocketSendException("Failed to send message to single connection; connection key not set.", exchange);
}
}
}
public WebsocketEndpoint getEndpoint() {
return endpoint;
}
@Override
public void doStart() throws Exception {
super.doStart();
endpoint.connect(this);
}
@Override
public void doStop() throws Exception {
endpoint.disconnect(this);
super.doStop();
}
boolean isSendToAllSet(Message in) {
// header may be null; have to be careful here (and fallback to use sendToAll option configured from endpoint)
Boolean value = in.getHeader(WebsocketConstants.SEND_TO_ALL, sendToAll, Boolean.class);
return value == null ? false : value;
}
void sendToAll(WebsocketStore store, Object message, Exchange exchange) throws Exception {
log.debug("Sending to all {}", message);
Collection<DefaultWebsocket> websockets = store.getAll();
Exception exception = null;
List<Future> futures = new CopyOnWriteArrayList<>();
for (DefaultWebsocket websocket : websockets) {
boolean isOkToSendMessage = false;
if (endpoint.getResourceUri() == null) {
isOkToSendMessage = true;
} else if (websocket.getPathSpec().equals(WebsocketComponent.createPathSpec(endpoint.getResourceUri()))) {
isOkToSendMessage = true;
}
if (isOkToSendMessage) {
try {
Future<Void> future = sendMessage(websocket, message);
if (future != null) {
futures.add(future);
}
} catch (Exception e) {
if (exception == null) {
exception = new WebsocketSendException("Failed to deliver message to one or more recipients.", exchange, e);
}
}
}
}
// check if they are all done within the timed out period
StopWatch watch = new StopWatch();
int timeout = endpoint.getSendTimeout();
while (!futures.isEmpty() && watch.taken() < timeout) {
// remove all that are done/cancelled
for (Future future : futures) {
if (future.isDone() || future.isCancelled()) {
futures.remove(future);
}
// if there are still more then we need to wait a little bit before checking again, to avoid burning cpu cycles in the while loop
if (!futures.isEmpty()) {
long interval = Math.min(1000, timeout);
log.debug("Sleeping {} millis waiting for sendToAll to complete sending with timeout {} millis", interval, timeout);
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
handleSleepInterruptedException(e, exchange);
}
}
}
}
if (!futures.isEmpty()) {
exception = new WebsocketSendException("Failed to deliver message within " + endpoint.getSendTimeout() + " millis to one or more recipients.", exchange);
}
if (exception != null) {
throw exception;
}
}
Future<Void> sendMessage(DefaultWebsocket websocket, Object message) throws IOException {
Future<Void> future = null;
// in case there is web socket and socket connection is open - send message
if (websocket != null && websocket.getSession().isOpen()) {
log.trace("Sending to websocket {} -> {}", websocket.getConnectionKey(), message);
if (message instanceof String) {
future = websocket.getSession().getRemote().sendStringByFuture((String) message);
} else if (message instanceof byte[]) {
ByteBuffer buf = ByteBuffer.wrap((byte[]) message);
future = websocket.getSession().getRemote().sendBytesByFuture(buf);
}
}
return future;
}
//Store is set/unset upon connect/disconnect of the producer
public void setStore(WebsocketStore store) {
this.store = store;
}
/**
* Called when a sleep is interrupted; allows derived classes to handle this case differently
*/
protected void handleSleepInterruptedException(InterruptedException e, Exchange exchange) throws InterruptedException {
if (log.isDebugEnabled()) {
log.debug("Sleep interrupted, are we stopping? {}", isStopping() || isStopped());
}
Thread.currentThread().interrupt();
throw e;
}
}