package org.yamcs.web.websocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.protobuf.Archive.ColumnData;
import org.yamcs.protobuf.Archive.StreamData;
import org.yamcs.protobuf.Rest.StreamSubscribeRequest;
import org.yamcs.protobuf.SchemaArchive;
import org.yamcs.protobuf.SchemaRest;
import org.yamcs.protobuf.Web.WebSocketServerMessage.WebSocketReplyData;
import org.yamcs.protobuf.Yamcs.ProtoDataType;
import org.yamcs.protobuf.Yamcs.Value;
import org.yamcs.protobuf.Yamcs.Value.Type;
import org.yamcs.web.rest.archive.ArchiveHelper;
import org.yamcs.yarch.ColumnDefinition;
import org.yamcs.yarch.DataType;
import org.yamcs.yarch.Stream;
import org.yamcs.yarch.StreamSubscriber;
import org.yamcs.yarch.Tuple;
import org.yamcs.yarch.TupleDefinition;
import org.yamcs.yarch.YarchDatabase;
/**
* Capable of producing and consuming yarch Stream data (Tuples) over web socket
*/
public class StreamResource extends AbstractWebSocketResource {
private static final Logger log = LoggerFactory.getLogger(StreamResource.class);
public static final String RESOURCE_NAME = "stream";
public static final String OP_subscribe = "subscribe";
public static final String OP_publish = "publish";
private List<Subscription> subscriptions = new ArrayList<>();
public StreamResource(WebSocketProcessorClient client) {
super(client);
}
@Override
public WebSocketReplyData processRequest(WebSocketDecodeContext ctx, WebSocketDecoder decoder) throws WebSocketException {
switch (ctx.getOperation()) {
case OP_subscribe:
return processSubscribeRequest(ctx, decoder);
case OP_publish:
return processPublishRequest(ctx, decoder);
default:
throw new WebSocketException(ctx.getRequestId(), "Unsupported operation '" + ctx.getOperation() + "'");
}
}
private WebSocketReplyData processSubscribeRequest(WebSocketDecodeContext ctx, WebSocketDecoder decoder) throws WebSocketException {
YarchDatabase ydb = YarchDatabase.getInstance(processor.getInstance());
// Optionally read body. If it's not provided, suppose the subscription concerns
// the stream of the current processor (TODO currently doesn't work with JSON).
Stream stream;
if (ctx.getData() != null) { // Check doesn't work with JSON, always returns JsonParser
StreamSubscribeRequest req = decoder.decodeMessageData(ctx, SchemaRest.StreamSubscribeRequest.MERGE).build();
if (req.hasStream()) {
stream = ydb.getStream(req.getStream());
} else {
throw new WebSocketException(ctx.getRequestId(), "No stream was provided");
}
} else {
stream = ydb.getStream(processor.getName());
}
StreamSubscriber subscriber = new StreamSubscriber() {
@Override
public void onTuple(Stream stream, Tuple tuple) {
StreamData data = ArchiveHelper.toStreamData(stream, tuple);
try {
wsHandler.sendData(ProtoDataType.STREAM_DATA, data, SchemaArchive.StreamData.WRITE);
} catch (IOException e) {
log.debug("Could not send tuple data", e);
}
}
@Override
public void streamClosed(Stream stream) {
}
};
stream.addSubscriber(subscriber);
subscriptions.add(new Subscription(stream, subscriber));
return toAckReply(ctx.getRequestId());
}
private static DataType dataTypeFromValue(Value value) {
switch (value.getType()) {
case SINT32:
return DataType.INT;
case DOUBLE:
return DataType.DOUBLE;
case BINARY:
return DataType.BINARY;
case TIMESTAMP:
return DataType.TIMESTAMP;
case STRING:
return DataType.STRING;
default:
throw new IllegalArgumentException("Unexpected value type " + value.getType());
}
}
private Object makeTupleColumn(WebSocketDecodeContext ctx, String name, Value value, DataType columnType) throws WebSocketException {
// Sanity check. We should perhaps find a better way to do all of this
switch (columnType.val) {
case SHORT:
if (value.getType() != Type.SINT32)
throw new WebSocketException(ctx.getRequestId(), String.format(
"Value type for column %s should be '%s'", name, Type.SINT32));
return value.getSint32Value();
case DOUBLE:
if (value.getType() != Type.DOUBLE)
throw new WebSocketException(ctx.getRequestId(), String.format(
"Value type for column %s should be '%s'", name, Type.DOUBLE));
return value.getDoubleValue();
case BINARY:
if (value.getType() != Type.BINARY)
throw new WebSocketException(ctx.getRequestId(), String.format(
"Value type for column %s should be '%s'", name, Type.BINARY));
return value.getBinaryValue().toByteArray();
case INT:
if (value.getType() != Type.SINT32)
throw new WebSocketException(ctx.getRequestId(), String.format(
"Value type for column %s should be '%s'", name, Type.SINT32));
return value.getSint32Value();
case TIMESTAMP:
if (value.getType() != Type.TIMESTAMP)
throw new WebSocketException(ctx.getRequestId(), String.format(
"Value type for column %s should be '%s'", name, Type.TIMESTAMP));
return value.getTimestampValue();
case ENUM:
case STRING:
if (value.getType() != Type.STRING)
throw new WebSocketException(ctx.getRequestId(), String.format(
"Value type for column %s should be '%s'", name, Type.STRING));
return value.getStringValue();
default:
throw new IllegalArgumentException("Tuple column type " + columnType.val + " is currently not supported");
}
}
private WebSocketReplyData processPublishRequest(WebSocketDecodeContext ctx, WebSocketDecoder decoder) throws WebSocketException {
YarchDatabase ydb = YarchDatabase.getInstance(processor.getInstance());
StreamData req = decoder.decodeMessageData(ctx, SchemaArchive.StreamData.MERGE).build();
Stream stream = ydb.getStream(req.getStream());
if (stream == null) {
throw new WebSocketException(ctx.getRequestId(), "Cannot find stream '" + req.getStream() + "'");
}
TupleDefinition tdef = stream.getDefinition();
List<Object> tupleColumns = new ArrayList<>();
// 'fixed' colums
for (ColumnDefinition cdef : stream.getDefinition().getColumnDefinitions()) {
ColumnData providedField = findColumnValue(req, cdef.getName());
if (providedField == null) continue;
if (!providedField.hasValue()) {
throw new WebSocketException(ctx.getRequestId(), "No value was provided for column " + cdef.getName());
}
Object column = makeTupleColumn(ctx, cdef.getName(), providedField.getValue(), cdef.getType());
tupleColumns.add(column);
}
// 'dynamic' columns
for (ColumnData val : req.getColumnList()) {
if (stream.getDefinition().getColumn(val.getName()) == null) {
DataType type = dataTypeFromValue(val.getValue());
tdef.addColumn(val.getName(), type);
Object column = makeTupleColumn(ctx, val.getName(), val.getValue(), type);
tupleColumns.add(column);
}
}
Tuple t = new Tuple(tdef, tupleColumns);
log.info("Emitting tuple {} to {}", t, stream.getName());
stream.emitTuple(t);
return toAckReply(ctx.getRequestId());
}
private static ColumnData findColumnValue(StreamData tupleData, String name) {
for (ColumnData val : tupleData.getColumnList()) {
if (val.getName().equals(name)) {
return val;
}
}
return null;
}
@Override
public void quit() {
for (Subscription subscription : subscriptions) {
subscription.stream.removeSubscriber(subscription.subscriber);
}
}
private static class Subscription {
Stream stream;
StreamSubscriber subscriber;
Subscription(Stream stream, StreamSubscriber subscriber) {
this.stream = stream;
this.subscriber = subscriber;
}
}
}