package org.yamcs.web.websocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.InvalidIdentification;
import org.yamcs.InvalidRequestIdentification;
import org.yamcs.NoPermissionException;
import org.yamcs.ProcessorException;
import org.yamcs.Processor;
import org.yamcs.parameter.ParameterRequestManagerImpl;
import org.yamcs.parameter.ParameterValue;
import org.yamcs.parameter.ParameterValueWithId;
import org.yamcs.parameter.ParameterWithIdConsumer;
import org.yamcs.parameter.ParameterWithIdRequestHelper;
import org.yamcs.protobuf.Comp.ComputationDef;
import org.yamcs.protobuf.Comp.ComputationDefList;
import org.yamcs.protobuf.Pvalue.ParameterData;
import org.yamcs.protobuf.SchemaComp;
import org.yamcs.protobuf.SchemaPvalue;
import org.yamcs.protobuf.SchemaWeb;
import org.yamcs.protobuf.SchemaYamcs;
import org.yamcs.protobuf.Web.ParameterSubscriptionRequest;
import org.yamcs.protobuf.Web.WebSocketServerMessage.WebSocketReplyData;
import org.yamcs.protobuf.Yamcs.NamedObjectId;
import org.yamcs.protobuf.Yamcs.NamedObjectList;
import org.yamcs.protobuf.Yamcs.ProtoDataType;
import org.yamcs.protobuf.Yamcs.StringMessage;
import org.yamcs.security.AuthenticationToken;
import org.yamcs.utils.StringConverter;
import org.yamcs.web.Computation;
import org.yamcs.web.ComputationFactory;
/**
* Provides realtime parameter subscription via web.
*
* TODO better deal with exceptions
*
* @author nm
*
*/
public class ParameterResource extends AbstractWebSocketResource implements ParameterWithIdConsumer {
private static final Logger log = LoggerFactory.getLogger(ParameterResource.class);
public static final String RESOURCE_NAME = "parameter";
public static final String WSR_subscribe = "subscribe";
public static final String WSR_unsubscribe = "unsubscribe";
public static final String WSR_subscribeAll = "subscribeAll";
public static final String WSR_unsubscribeAll = "unsubscribeAll";
private int subscriptionId = -1;
private int compSubscriptionId = -1; //subscription id used for computations
final CopyOnWriteArrayList<Computation> compList=new CopyOnWriteArrayList<>();
ParameterWithIdRequestHelper pidrm;
public ParameterResource(WebSocketProcessorClient client) {
super(client);
pidrm = new ParameterWithIdRequestHelper(processor.getParameterRequestManager(), this);
}
@Override
public WebSocketReplyData processRequest(WebSocketDecodeContext ctx, WebSocketDecoder decoder) throws WebSocketException {
switch (ctx.getOperation()) {
case WSR_subscribe:
NamedObjectList subscribeList = decoder.decodeMessageData(ctx, SchemaYamcs.NamedObjectList.MERGE).build();
return subscribe(ctx.getRequestId(), subscribeList, client.getAuthToken());
case "subscribe2":
// TODO Experimental, but intended to replace WSR_subscribe in a next version. Provides same functionality
// but with the option to ignore invalid parameters and continue with the subscription
ParameterSubscriptionRequest req = decoder.decodeMessageData(ctx, SchemaWeb.ParameterSubscriptionRequest.MERGE).build();
return subscribe2(ctx.getRequestId(), req, client.getAuthToken());
case WSR_subscribeAll:
StringMessage stringMessage = decoder.decodeMessageData(ctx, SchemaYamcs.StringMessage.MERGE).build();
return subscribeAll(ctx.getRequestId(), stringMessage.getMessage(), client.getAuthToken());
case WSR_unsubscribe:
NamedObjectList unsubscribeList = decoder.decodeMessageData(ctx, SchemaYamcs.NamedObjectList.MERGE).build();
return unsubscribe(ctx.getRequestId(), unsubscribeList, client.getAuthToken());
case WSR_unsubscribeAll:
return unsubscribeAll(ctx.getRequestId());
case "subscribeComputations":
ComputationDefList cdefList = decoder.decodeMessageData(ctx, SchemaComp.ComputationDefList.MERGE).build();
return subscribeComputations(ctx.getRequestId(), cdefList, client.getAuthToken());
default:
throw new WebSocketException(ctx.getRequestId(), "Unsupported operation '" + ctx.getOperation() + "'");
}
}
@Deprecated
private WebSocketReplyData subscribe(int requestId, NamedObjectList paraList, AuthenticationToken authToken) throws WebSocketException {
List<NamedObjectId> idlist = paraList.getListList();
try {
if(subscriptionId!=-1) {
pidrm.addItemsToRequest(subscriptionId, idlist, authToken);
} else {
subscriptionId=pidrm.addRequest(idlist, authToken);
}
WebSocketReplyData reply = toAckReply(requestId);
wsHandler.sendReply(reply);
if(pidrm.hasParameterCache()) {
List<ParameterValueWithId> pvlist = pidrm.getValuesFromCache(idlist, authToken);
if(!pvlist.isEmpty()) {
update(subscriptionId, pvlist);
}
}
return null;
} catch (InvalidIdentification e) {
NamedObjectList nol = NamedObjectList.newBuilder().addAllList(e.getInvalidParameters()).build();
WebSocketException ex = new WebSocketException(requestId, e);
ex.attachData("InvalidIdentification", nol, SchemaYamcs.NamedObjectList.WRITE);
throw ex;
} catch (InvalidRequestIdentification e) {
log.error("got invalid subscription id", e);
throw new WebSocketException(requestId, "internal error: "+e.toString(), e);
} catch (IOException e) {
log.error("Exception when sending data", e);
return null;
} catch (NoPermissionException e) {
log.warn("no permission for parameters: {}", e.getMessage());
throw new WebSocketException(requestId, "internal error: "+e.toString(), e);
}
}
private WebSocketReplyData subscribe2(int requestId, ParameterSubscriptionRequest req, AuthenticationToken authToken) throws WebSocketException {
List<NamedObjectId> idList = req.getIdList();
try {
try {
if(subscriptionId!=-1) {
pidrm.addItemsToRequest(subscriptionId, idList, authToken);
} else {
subscriptionId=pidrm.addRequest(idList, authToken);
}
} catch (InvalidIdentification e) {
if (req.hasAbortOnInvalid() && req.getAbortOnInvalid()) {
NamedObjectList nol = NamedObjectList.newBuilder().addAllList(e.getInvalidParameters()).build();
WebSocketException ex = new WebSocketException(requestId, e);
ex.attachData("InvalidIdentification", nol, SchemaYamcs.NamedObjectList.WRITE);
throw ex;
} else {
idList = new ArrayList<>(idList);
idList.removeAll(e.getInvalidParameters());
if (idList.isEmpty()) {
log.warn("Received subscribe attempt will all-invalid parameters");
} else {
log.warn("Received subscribe attempt with {} invalid parameters. Subscription will continue with {} remaining valids.",
e.getInvalidParameters().size(), idList.size());
if (log.isDebugEnabled()) {
log.debug("The invalid IDs are: {}", StringConverter.idListToString(e.getInvalidParameters()));
}
if(subscriptionId!=-1) {
pidrm.addItemsToRequest(subscriptionId, idList, authToken);
} else {
subscriptionId=pidrm.addRequest(idList, authToken);
}
}
// TODO send back invalid list as part of nominal response. Requires work in the websocket framework which
// currently only supports ACK responses in the reply itself
}
}
WebSocketReplyData reply = toAckReply(requestId);
wsHandler.sendReply(reply);
if(pidrm.hasParameterCache()) {
List<ParameterValueWithId> pvlist = pidrm.getValuesFromCache(idList, authToken);
if(!pvlist.isEmpty()) {
update(subscriptionId, pvlist);
}
}
return null;
} catch (InvalidIdentification e) {
log.warn("got invalid identification: {}", e.getMessage());
throw new WebSocketException(requestId, "internal error: "+e.toString(), e);
} catch (InvalidRequestIdentification e) {
log.error("got invalid subscription id", e);
throw new WebSocketException(requestId, "internal error: "+e.toString(), e);
} catch (NoPermissionException e) {
log.error("no permission for parameters: {}", e.getMessage());
throw new WebSocketException(requestId, "internal error: "+e.toString(), e);
} catch (IOException e) {
log.error("Exception when sending data", e);
return null;
}
}
private WebSocketReplyData unsubscribe(int requestId, NamedObjectList paraList, AuthenticationToken authToken) throws WebSocketException {
if(subscriptionId!=-1) {
try {
pidrm.removeItemsFromRequest(subscriptionId, paraList.getListList(), authToken);
} catch (NoPermissionException e) {
throw new WebSocketException(requestId, "No permission", e);
}
return toAckReply(requestId);
} else {
throw new WebSocketException(requestId, "Not subscribed to anything");
}
}
private WebSocketReplyData subscribeAll(int requestId, String namespace, AuthenticationToken authToken) throws WebSocketException {
if(subscriptionId!=-1) {
throw new WebSocketException(requestId, "Already subscribed for this client");
}
try {
subscriptionId=pidrm.subscribeAll(namespace, authToken);
} catch (NoPermissionException e) {
throw new WebSocketException(requestId, "No permission", e);
}
return toAckReply(requestId);
}
private WebSocketReplyData subscribeComputations(int requestId, ComputationDefList cdefList, AuthenticationToken authToken)
throws WebSocketException {
List<ComputationDef> computations = cdefList.getComputationList();
List<NamedObjectId> allArguments=new ArrayList<>();
for(ComputationDef computation : computations) {
allArguments.addAll(computation.getArgumentList());
}
try {
try {
if(compSubscriptionId!=-1) {
pidrm.addItemsToRequest(compSubscriptionId, allArguments, authToken);
} else {
compSubscriptionId=pidrm.addRequest(allArguments, authToken);
}
} catch (InvalidIdentification e) {
if (cdefList.hasAbortOnInvalid() && cdefList.getAbortOnInvalid()) {
NamedObjectList nol=NamedObjectList.newBuilder().addAllList(e.getInvalidParameters()).build();
WebSocketException ex = new WebSocketException(requestId, e);
ex.attachData("InvalidIdentification", nol, SchemaYamcs.NamedObjectList.WRITE);
throw ex;
} else {
//remove computations that have as arguments the invalid parameters
computations = new ArrayList<>();
allArguments = new ArrayList<>();
ListIterator<ComputationDef> it = computations.listIterator();
while (it.hasNext()) {
ComputationDef computation = it.next();
boolean remove = false;
for (NamedObjectId argId : computation.getArgumentList()) {
if (e.getInvalidParameters().contains(argId)) {
remove = true;
break;
}
}
if (remove) {
it.remove();
} else {
allArguments.addAll(computation.getArgumentList());
}
}
if (computations.isEmpty()) {
log.warn("All requested computations have invalid arguments");
} else {
log.warn("Got invalid computation arguments, but continuing subscribe attempt with remaining valids: {} ", computations);
if(compSubscriptionId!=-1) {
pidrm.addItemsToRequest(compSubscriptionId, allArguments, authToken);
} else {
compSubscriptionId=pidrm.addRequest(allArguments, authToken);
}
}
// TODO send back invalid list as part of nominal response. Requires work in the websocket framework which
// currently only supports ACK responses in the reply itself
}
}
} catch (InvalidIdentification e) {
log.error("got invalid identification. Should not happen, because checked before", e);
throw new WebSocketException(requestId, "internal error: "+e.toString(), e);
} catch (NoPermissionException e) {
throw new WebSocketException(requestId, "No permission", e);
}
try {
for(ComputationDef cdef : computations) {
Computation c = ComputationFactory.getComputation(cdef);
compList.add(c);
}
} catch (Exception e) {
log.warn("Cannot create computation: ", e);
throw new WebSocketException(requestId, "Could not create computation", e);
}
return toAckReply(requestId);
}
private WebSocketReplyData unsubscribeAll(int requestId) throws WebSocketException {
if(subscriptionId==-1) {
throw new WebSocketException(requestId, "Not subscribed");
}
ParameterRequestManagerImpl prm=processor.getParameterRequestManager();
boolean r=prm.unsubscribeAll(subscriptionId);
if(r) {
subscriptionId=-1;
return toAckReply(requestId);
} else {
throw new WebSocketException(requestId, "There is no subscribeAll subscription for this client");
}
}
@Override
public void update(int subscrId, List<ParameterValueWithId> paramList) {
if(wsHandler==null) {
return;
}
if(paramList == null || paramList.isEmpty()) {
return;
}
if(subscrId==compSubscriptionId) {
updateComputations(paramList);
return;
}
ParameterData.Builder pd=ParameterData.newBuilder();
for(ParameterValueWithId pvwi:paramList) {
ParameterValue pv=pvwi.getParameterValue();
pd.addParameter(pv.toGpb(pvwi.getId()));
}
try {
wsHandler.sendData(ProtoDataType.PARAMETER, pd.build(), SchemaPvalue.ParameterData.WRITE);
} catch (Exception e) {
log.warn("got error when sending parameter updates, quitting", e);
quit();
}
}
private void updateComputations(List<ParameterValueWithId> paramList) {
Map<NamedObjectId, ParameterValue> parameters=new HashMap<>();
for(ParameterValueWithId pvwi:paramList) {
parameters.put(pvwi.getId(), pvwi.getParameterValue());
}
ParameterData.Builder pd=ParameterData.newBuilder();
for(Computation c:compList) {
org.yamcs.protobuf.Pvalue.ParameterValue pv=c.evaluate(parameters);
if(pv!=null) {
pd.addParameter(pv);
}
}
if(pd.getParameterCount()==0) {
return;
}
try {
wsHandler.sendData(ProtoDataType.PARAMETER, pd.build(), SchemaPvalue.ParameterData.WRITE);
} catch (Exception e) {
log.warn("got error when sending parameter updates, quitting", e);
quit();
}
}
/**
* called when the socket is closed.
* unsubscribe all parameters
*/
@Override
public void quit() {
ParameterRequestManagerImpl prm=processor.getParameterRequestManager();
if(subscriptionId!=-1) {
prm.removeRequest(subscriptionId);
}
if(compSubscriptionId!=-1) {
prm.removeRequest(compSubscriptionId);
}
}
@Override
public void switchProcessor(Processor oldProcessor, Processor newProcessor) throws ProcessorException {
try {
pidrm.switchPrm(newProcessor.getParameterRequestManager(), client.getAuthToken());
super.switchProcessor(oldProcessor, newProcessor);
} catch (InvalidIdentification e) {
log.warn("got InvalidIdentification when resubscribing", e);
} catch (NoPermissionException e) {
throw new ProcessorException("No permission", e);
}
}
}