package org.yamcs.web.websocket;
import io.protostuff.JsonIOUtil;
import io.protostuff.Schema;
import com.google.protobuf.MessageLite;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.yamcs.api.ws.WSConstants;
public class JsonDecoder implements WebSocketDecoder {
private JsonFactory jsonFactory = new JsonFactory();
/**
* Decodes the first few common wrapper fields of an incoming web socket message.<br>
* Sample: [1,1,2,{"<resource>":"<operation>", "data": <undecoded remainder>}]
*/
@Override
public WebSocketDecodeContext decodeMessage(InputStream in) throws WebSocketException {
int requestId = WSConstants.NO_REQUEST_ID;
try {
JsonParser jsp = jsonFactory.createParser(in);
requireTerminalToken(requestId, jsp, JsonToken.START_ARRAY);
// PROTOCOL VERSION
requireToken(requestId, jsp, JsonToken.VALUE_NUMBER_INT, "version");
int version = jsp.getIntValue();
if (version != WSConstants.PROTOCOL_VERSION)
throw new WebSocketException(requestId, "Invalid version (expected " + WSConstants.PROTOCOL_VERSION + ", but got " + version);
// MESSAGE TYPE. Currently fixed for client-to-server messages
requireToken(requestId, jsp, JsonToken.VALUE_NUMBER_INT, "message type");
int messageType = jsp.getIntValue();
if (messageType != WSConstants.MESSAGE_TYPE_REQUEST) {
throw new WebSocketException(requestId, "Invalid message type (expected " + WSConstants.MESSAGE_TYPE_REQUEST + ", but got " + messageType);
}
// SEQUENCE NUMBER. Check whether positive, because we use -1 for indicating that there is no known valid request-id
requireToken(requestId, jsp, JsonToken.VALUE_NUMBER_INT, "requestId");
int candidateRequestId = jsp.getIntValue();
if (candidateRequestId < 0) {
throw new WebSocketException(requestId, "Invalid requestId. This needs to be a positive number");
}
requestId = candidateRequestId;
// TODO not sure why this is an object, maybe because it contains options?
requireTerminalToken(requestId, jsp, JsonToken.START_OBJECT);
// RESOURCE
requireToken(requestId, jsp, JsonToken.FIELD_NAME, "resource");
String resource = jsp.getCurrentName();
// OPERATION ON THAT RESOURCE
requireToken(requestId, jsp, JsonToken.VALUE_STRING, "operation");
String operation = jsp.getText();
WebSocketDecodeContext ctx = new WebSocketDecodeContext(version, messageType, requestId, resource, operation);
// Position JsonParser so that it point directly to the actual message (if any)
if (jsp.nextToken() != null) {
if ((jsp.getCurrentToken()==JsonToken.FIELD_NAME) && (!"data".equals(jsp.getCurrentName()))) {
throw new WebSocketException(requestId, "Invalid message (expecting data as the next field)");
}
}
// JsonParser is greedy, so hide our parser back in the returned message for later reuse
// alternative is releaseBuffered, but even with that it chops off the START_OBJECT
ctx.setData(jsp);
return ctx;
} catch (IOException e) {
throw new WebSocketException(requestId, e);
}
}
protected static void requireToken(int requestId, JsonParser jsp, JsonToken token, String type) throws IOException, WebSocketException {
if (jsp.nextToken() != token) {
JsonLocation loc = jsp.getCurrentLocation();
throw new WebSocketException(requestId, String.format(
"Invalid message at line %d column %d: Expected '%s' token for %s but got '%s' instead)",
loc.getLineNr(), loc.getColumnNr(), token.asString(), type, jsp.getCurrentToken()));
}
}
protected static void requireTerminalToken(int requestId, JsonParser jsp, JsonToken token) throws IOException, WebSocketException {
if (jsp.nextToken() != token) {
JsonLocation loc = jsp.getCurrentLocation();
String expected = (token.asString() == null) ? token.toString() : token.asString();
String actual = (jsp.getCurrentToken().asString() == null) ? jsp.getCurrentToken().toString() : jsp.getCurrentToken().asString();
throw new WebSocketException(requestId, String.format(
"Invalid message at line %d column %d: Expected '%s' token but got '%s' instead)",
loc.getLineNr(), loc.getColumnNr(), expected, actual));
}
}
@Override
public <T extends MessageLite.Builder> T decodeMessageData(WebSocketDecodeContext ctx, Schema<T> schema) throws WebSocketException {
// Re-use our earlier JsonParser, since that is correctly positioned
try {
T msg = schema.newMessage();
JsonIOUtil.mergeFrom((JsonParser) ctx.getData(), msg, schema, false);
return msg;
} catch (IOException e) {
throw new WebSocketException(ctx.getRequestId(), e);
}
}
public static void main(String... args) throws WebSocketException {
WebSocketDecodeContext ctx =
new JsonDecoder().decodeMessage(new ByteArrayInputStream("[1,1,3,{\"cmdhistory\":\"subscribe\"}]".getBytes()));
System.out.println("ctx "+ctx.getRequestId());
}
}