/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core.notification;
import org.eclipse.che.commons.lang.Pair;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.everrest.websockets.client.BaseClientMessageListener;
import org.everrest.websockets.client.WSClient;
import org.everrest.websockets.message.JsonMessageConverter;
import org.everrest.websockets.message.MessageConversionException;
import org.everrest.websockets.message.MessageConverter;
import org.everrest.websockets.message.RESTfulOutputMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Receives event over websocket and publish them to the local EventsService.
*
* @author andrew00x
*/
@Singleton
public final class WSocketEventBusClient {
private static final Logger LOG = LoggerFactory.getLogger(WSocketEventBusClient.class);
private static final long wsConnectionTimeout = 2000;
private final EventService eventService;
private final Pair<String, String>[] eventSubscriptions;
private final ClientEventPropagationPolicy policy;
private final MessageConverter messageConverter;
private final ConcurrentMap<URI, Future<WSClient>> connections;
private final AtomicBoolean start;
private ExecutorService executor;
@Inject
public WSocketEventBusClient(EventService eventService,
@Nullable @Named("notification.client.event_subscriptions") Pair<String, String>[] eventSubscriptions,
@Nullable ClientEventPropagationPolicy policy) {
this.eventService = eventService;
this.eventSubscriptions = eventSubscriptions;
this.policy = policy;
messageConverter = new JsonMessageConverter();
connections = new ConcurrentHashMap<>();
start = new AtomicBoolean(false);
}
@PostConstruct
void start() {
if (start.compareAndSet(false, true)) {
if (policy != null) {
eventService.subscribe(new EventSubscriber<Object>() {
@Override
public void onEvent(Object event) {
propagate(event);
}
});
}
if (eventSubscriptions != null) {
final Map<URI, Set<String>> cfg = new HashMap<>();
for (Pair<String, String> service : eventSubscriptions) {
try {
final URI key = new URI(service.first);
Set<String> values = cfg.get(key);
if (values == null) {
cfg.put(key, values = new LinkedHashSet<>());
}
if (service.second != null) {
values.add(service.second);
}
} catch (URISyntaxException e) {
LOG.error(e.getMessage(), e);
}
}
if (!cfg.isEmpty()) {
executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("WSocketEventBusClient")
.setDaemon(true).build());
for (Map.Entry<URI, Set<String>> entry : cfg.entrySet()) {
executor.execute(new ConnectTask(entry.getKey(), entry.getValue()));
}
}
}
}
}
protected void propagate(Object event) {
for (Future<WSClient> future : connections.values()) {
if (future.isDone()) {
try {
final WSClient client = future.get();
if (policy.shouldPropagated(client.getUri(), event)) {
client.send(messageConverter.toString(Messages.clientMessage(event)));
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
}
}
@PreDestroy
void stop() {
if (start.compareAndSet(true, false) && executor != null) {
executor.shutdownNow();
}
}
private void connect(final URI wsUri, final Collection<String> channels) throws IOException {
Future<WSClient> clientFuture = connections.get(wsUri);
if (clientFuture == null) {
FutureTask<WSClient> newFuture = new FutureTask<>(new Callable<WSClient>() {
@Override
public WSClient call() throws IOException, MessageConversionException {
WSClient wsClient = new WSClient(wsUri, new WSocketListener(wsUri, channels));
wsClient.connect(wsConnectionTimeout);
return wsClient;
}
});
clientFuture = connections.putIfAbsent(wsUri, newFuture);
if (clientFuture == null) {
clientFuture = newFuture;
newFuture.run();
}
}
boolean connected = false;
try {
clientFuture.get(); // wait for connection
connected = true;
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof Error) {
throw (Error)cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException)cause;
} else if (cause instanceof IOException) {
throw (IOException)cause;
}
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
if (!connected) {
connections.remove(wsUri);
}
}
}
private class WSocketListener extends BaseClientMessageListener {
final URI wsUri;
final Set<String> channels;
WSocketListener(URI wsUri, Collection<String> channels) {
this.wsUri = wsUri;
this.channels = new HashSet<>(channels);
}
@Override
public void onClose(int status, String message) {
connections.remove(wsUri);
LOG.debug("Close connection to {}. ", wsUri);
if (start.get()) {
executor.execute(new ConnectTask(wsUri, channels));
}
}
@Override
public void onMessage(String data) {
try {
final RESTfulOutputMessage message = messageConverter.fromString(data, RESTfulOutputMessage.class);
if (message != null && message.getHeaders() != null) {
for (org.everrest.websockets.message.Pair header : message.getHeaders()) {
if ("x-everrest-websocket-channel".equals(header.getName())) {
final String channel = header.getValue();
if (channel != null && channels.contains(channel)) {
final Object event = Messages.restoreEventFromBroadcastMessage(message);
if (event != null) {
eventService.publish(event);
}
}
}
}
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
@Override
public void onOpen(WSClient client) {
LOG.debug("Open connection to {}. ", wsUri);
for (String channel : channels) {
try {
client.send(messageConverter.toString(Messages.subscribeChannelMessage(channel)));
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
}
}
private class ConnectTask implements Runnable {
final URI wsUri;
final Collection<String> channels;
ConnectTask(URI wsUri, Collection<String> channels) {
this.wsUri = wsUri;
this.channels = channels;
}
@Override
public void run() {
for (; ; ) {
if (Thread.currentThread().isInterrupted()) {
return;
}
try {
connect(wsUri, channels);
return;
} catch (IOException e) {
LOG.error(String.format("Failed connect to %s", wsUri), e);
synchronized (this) {
try {
wait(wsConnectionTimeout * 2);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
}
}
}
}
}
}
}