All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.relayr.java.websocket.WebSocketClient Maven / Gradle / Ivy

package io.relayr.java.websocket;

import com.google.gson.Gson;

import org.eclipse.paho.client.mqttv3.MqttException;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.inject.Singleton;

import io.relayr.java.api.ChannelApi;
import io.relayr.java.model.DataPackage;
import io.relayr.java.model.Device;
import io.relayr.java.model.action.Action;
import io.relayr.java.model.action.Command;
import io.relayr.java.model.action.Configuration;
import io.relayr.java.model.action.Reading;
import io.relayr.java.model.channel.ChannelDefinition;
import io.relayr.java.model.channel.DataChannel;
import io.relayr.java.model.channel.PublishChannel;
import io.relayr.java.websocket.error.MqttDisconnectException;
import rx.Observable;
import rx.Observer;
import rx.Subscriber;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import rx.subjects.AsyncSubject;
import rx.subjects.PublishSubject;
import rx.subjects.ReplaySubject;
import rx.subjects.Subject;

import static io.relayr.java.model.channel.ChannelTransport.MQTT;

@Singleton
public class WebSocketClient {

    private final ChannelApi channelApi;
    private final WebSocket webSocket;
    private final Map subChannels = new HashMap<>();
    final Map> subObservers = new HashMap<>();

    final Map pubChannels = new HashMap<>();
    final Map> pubObservers = new HashMap<>();
    final Map> channelCreation = new HashMap<>();

    final Map actionObservers = new HashMap<>();

    @Inject
    public WebSocketClient(ChannelApi api, WebSocketFactory factory) {
        channelApi = api;
        webSocket = factory.createWebSocket();
    }

    /**
     * Subscribe to get {@link Reading} from device.
     * @param device device to subscribe to
     */
    public Observable subscribe(Device device) {
        if (device == null || device.getId() == null) return Observable.empty();
        return subscribe(device.getId());
    }

    /**
     * Subscribe to get {@link Reading} from device.
     * @param deviceId to subscribe to
     */
    public Observable subscribe(String deviceId) {
        if (deviceId == null) return Observable.empty();

        if (!subObservers.containsKey(deviceId)) return createSubObserver(deviceId);
        else return subObservers.get(deviceId);
    }

    private synchronized Observable createSubObserver(final String deviceId) {
        final PublishSubject subject = PublishSubject.create();
        subObservers.put(deviceId, subject);

        channelApi.create(new ChannelDefinition(deviceId, MQTT))
                .flatMap(new Func1>() {
                    @Override public Observable call(final DataChannel channel) {
                        return webSocket.createClient(channel);
                    }
                })
                .subscribeOn(Schedulers.newThread())
                .subscribe(new Subscriber() {
                    @Override public void onCompleted() {}

                    @Override public void onError(Throwable e) {
                        System.out.printf("Failed to create MQT client.");
                        subObservers.remove(deviceId);
                        subject.onError(e);
                    }

                    @Override public void onNext(DataChannel channel) {
                        subscribeToChannel(channel, deviceId, subject);
                    }
                });

        return subject.doOnError(new Action1() {
            @Override public void call(Throwable throwable) {
                unSubscribe(deviceId);
            }
        });
    }

    private void subscribeToChannel(final DataChannel channel, final String deviceId,
                                    final PublishSubject subject) {
        webSocket.subscribe(channel.getTopic(), channel.getId(), new WebSocketCallback() {
            @Override public void connectCallback(Object message) {
                if (!subChannels.containsKey(deviceId))
                    subChannels.put(deviceId, channel);
            }

            @Override public void disconnectCallback(Object message) {
                subject.onError((Throwable) message);
                subChannels.remove(deviceId);
                subObservers.remove(deviceId);
            }

            @Override public void successCallback(Object message) {
                DataPackage dataPackage = new Gson().fromJson(message.toString(), DataPackage.class);
                for (DataPackage.Data dataPoint : dataPackage.readings) {
                    subject.onNext(new Reading(dataPackage.received, dataPoint.recorded,
                            dataPoint.meaning, dataPoint.path, dataPoint.value));
                }
            }

            @Override
            public void errorCallback(Throwable e) {
                subject.onError(e);
                subChannels.remove(deviceId);
                subObservers.remove(deviceId);
            }
        });
    }

    public void unSubscribe(final String deviceId) {
        if (subObservers.containsKey(deviceId)) {
            subObservers.get(deviceId).onCompleted();
            subObservers.remove(deviceId);
        }

        if (!subChannels.isEmpty() && subChannels.containsKey(deviceId))
            if (webSocket.unSubscribe(subChannels.get(deviceId).getCredentials().getTopic()))
                subChannels.remove(deviceId);
    }

    public synchronized Observable publish(final String deviceId, final Reading payload) {
        final AsyncSubject publishSubject = pubObservers.get(deviceId);
        if (publishSubject != null) {
            publishData(deviceId, payload, publishSubject);
        } else {
            if (channelCreation.get(deviceId) != null) return Observable.empty();

            final AsyncSubject subject = AsyncSubject.create();
            createPubChannel(deviceId)
                    .subscribeOn(Schedulers.newThread())
                    .subscribe(new Observer() {
                        @Override public void onCompleted() {}

                        @Override public void onError(Throwable e) {
                            pubChannels.remove(deviceId);
                            pubObservers.remove(deviceId);
                            channelCreation.remove(deviceId);
                            subject.onError(e);
                        }

                        @Override public void onNext(DataChannel channel) {
                            if (!pubObservers.containsKey(deviceId))
                                pubObservers.put(deviceId, subject);
                            if (!pubChannels.containsKey(deviceId))
                                pubChannels.put(deviceId, channel);
                            channelCreation.remove(deviceId);

                            publishData(deviceId, payload, subject);
                        }
                    });

            return clearIfError(subject, deviceId);
        }

        return publishSubject;
    }

    private void publishData(String deviceId, Reading payload, AsyncSubject subject) {
        try {
            final boolean success = webSocket.publish(deviceId, pubChannels.get(deviceId).getCredentials().getTopic() + "data",
                    new Gson().toJson(payload));
            subject.onNext(success);
            subject.onCompleted(); //Called because async observable is used
        } catch (MqttException e) {
            removePublisher(deviceId);
            subject.onError(new Exception("WebSocketClient - MqttException", e));
        } catch (Exception ae) {
            removePublisher(deviceId);
            subject.onError(new Exception("WebSocketClient - Unknown exception", ae));
        }
    }

    /**
     * Subscribe to get {@link Action} from device.
     * {@link Action} can be {@link Command} or {@link Configuration}
     * @param deviceId to subscribe to
     */
    public Observable subscribeToCommands(String deviceId) {
        if (deviceId == null) return Observable.empty();

        if (!actionObservers.containsKey(deviceId)) {
            final PublishSubject subject = PublishSubject.create();
            return createActionObserver(deviceId, subject, Command.class);
        } else {
            return actionObservers.get(deviceId);
        }
    }

    /**
     * Subscribe to get {@link Action} from device.
     * {@link Action} can be {@link Command} or {@link Configuration}
     * @param deviceId to subscribe to
     */
    public Observable subscribeToConfigurations(String deviceId) {
        if (deviceId == null) return Observable.empty();

        if (!actionObservers.containsKey(deviceId)) {
            final PublishSubject subject = PublishSubject.create();
            return createActionObserver(deviceId, subject, Configuration.class);
        } else {
            return actionObservers.get(deviceId);
        }
    }

    synchronized  Observable createActionObserver(final String deviceId,
                                                        final PublishSubject subject,
                                                        final Class type) {
        actionObservers.put(deviceId, subject);

        createPubChannel(deviceId)
                .subscribeOn(Schedulers.newThread())
                .subscribe(new Subscriber() {
                    @Override public void onCompleted() {}

                    @Override public void onError(Throwable e) {
                        System.err.println("Failed to create publish channel");
                        subject.onError(e);
                        pubChannels.remove(deviceId);
                        actionObservers.remove(deviceId);
                        channelCreation.remove(deviceId);
                    }

                    @Override public void onNext(DataChannel channel) {
                        if (!pubChannels.containsKey(deviceId)) pubChannels.put(deviceId, channel);
                        channelCreation.remove(deviceId);

                        final String topic = channel.getTopic() + (type == Command.class ? "cmd" : "conf");
                        final boolean subscribe = webSocket.subscribeAction(topic,
                                channel.getCredentials().getUser(), channel.getId(),
                                new WebSocketCallback() {
                                    @Override public void connectCallback(Object message) {}

                                    @Override public void disconnectCallback(Object message) {
                                        actionObservers.remove(deviceId);
                                        subject.onError((Throwable) message);
                                    }

                                    @Override public void successCallback(Object message) {
                                        subject.onNext(new Gson().fromJson(message.toString(), type));
                                    }

                                    @Override
                                    public void errorCallback(Throwable e) {
                                        e.printStackTrace();
                                        actionObservers.remove(deviceId);
                                        subject.onError(e);
                                    }
                                });
                        if (!subscribe) {
                            actionObservers.remove(deviceId);
                            subject.onError(new Throwable("Failed to subscribe to MQTT"));
                        }
                    }
                });

        return clearIfError(subject, deviceId);
    }

    synchronized Observable createPubChannel(final String deviceId) {
        ReplaySubject subject = channelCreation.get(deviceId);
        if (subject == null) {
            subject = ReplaySubject.create();
            channelCreation.put(deviceId, subject);

            channelApi.createForDevice(new ChannelDefinition(deviceId, MQTT), deviceId)
                    .timeout(7, TimeUnit.SECONDS)
                    .subscribeOn(Schedulers.newThread())
                    .flatMap(new Func1>() {
                        @Override public Observable call(PublishChannel channel) {
                            return webSocket.createPublishClient(channel);
                        }
                    })
                    .subscribe(new Action1() {
                        @Override public void call(DataChannel dataChannel) {
                            channelCreation.get(deviceId).onNext(dataChannel);
                        }
                    }, new Action1() {
                        @Override public void call(Throwable e) {
                            System.err.println("Failed to create publish MQTT client");
                            channelCreation.get(deviceId).onError(e);
                            removePublisher(deviceId);
                        }
                    });
        }

        return channelCreation.get(deviceId);
    }

    private  Observable clearIfError(Subject subject, final String deviceId) {
        return subject.doOnError(new Action1() {
            @Override public void call(Throwable throwable) {
                pubChannels.remove(deviceId);
                pubObservers.remove(deviceId);
            }
        });
    }

    public void clean() {
        if (webSocket != null) webSocket.reset();
        if (subChannels != null) subChannels.clear();
        if (subObservers != null) subObservers.clear();
        if (pubChannels != null) pubChannels.clear();
        if (pubObservers != null) pubObservers.clear();
        if (channelCreation != null) channelCreation.clear();
        if (actionObservers != null) actionObservers.clear();
    }

    public void removePublisher(String deviceId) {
        if (pubChannels.get(deviceId) != null) pubChannels.remove(deviceId);
        if (pubObservers.get(deviceId) != null) pubObservers.remove(deviceId);
        if (channelCreation.get(deviceId) != null) channelCreation.remove(deviceId);
        if (webSocket != null) webSocket.removePublishClient(deviceId);
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy