/*
# Licensed Materials - Property of IBM
# Copyright IBM Corp. 2013, 2013
*/
package com.ibm.streamsx.inet.wsserver;
import org.apache.log4j.Logger;
import com.ibm.streams.operator.OperatorContext;
import com.ibm.streams.operator.OutputTuple;
import com.ibm.streams.operator.StreamingOutput;
import com.ibm.streams.operator.Type;
import com.ibm.streams.operator.log4j.TraceLevel;
import com.ibm.streams.operator.metrics.Metric;
import com.ibm.streams.operator.metrics.Metric.Kind;
import com.ibm.streams.operator.model.CustomMetric;
import com.ibm.streams.operator.model.Libraries;
import com.ibm.streams.operator.model.OutputPortSet;
import com.ibm.streams.operator.model.OutputPortSet.WindowPunctuationOutputMode;
import com.ibm.streams.operator.model.OutputPorts;
import com.ibm.streams.operator.model.Parameter;
import com.ibm.streams.operator.model.PrimitiveOperator;
import com.ibm.streams.operator.model.Icons;
import com.ibm.streams.operator.samples.patterns.TupleProducer;
/* TODO - enable ability to specify the network device the operator listens on */
/**
* A source type operator that receives websockets messages from multiple clients.
* Each message arriving on the WebSocket is converted to a tuple and injected into
* the stream.
*
* Optionally the tuple being injected can include and identifier of the sender,
* the identifier is unique for the lifetime of the session.
*
*/
@PrimitiveOperator(description=WebSocketInject.primDesc)
@OutputPorts({@OutputPortSet(description=WebSocketInject.outPortDesc, cardinality=1, optional=false, windowPunctuationOutputMode=WindowPunctuationOutputMode.Free)})
@Libraries("opt/wssupport/Java-WebSocket-1.3.0.jar")
@Icons(location32="icons/WebSocketInject_32.gif", location16="icons/WebSocketInject_16.gif")
public class WebSocketInject extends TupleProducer {
final static String primDesc =
" Operator recieves messages from WebSocket clients and generates a tuple which is sent to streams. " +
" Each received message is output as tuple. The data received is dependent upon" +
" the input ports schema.";
final static String outPortDesc =
"First attribute will have the message received via the WebSocket, of type rstring. " +
"Second attribute (if provided) will have the senders unique id, or type rstring." +
"Subsequent attribute(s) are allowed and will not be poplulated.";
final static String parmPortDesc =
"WebSocket network port that messages arrive on. The WebSocket client(s) " +
"use this port to transmit on.";
final static String parmAckDesc =
"The operator sends out an ack message to all currently connected clients. " +
"An ack message is sent when the (totaslNumberOfMessagesRecieved % ackCount) == 0, " +
"The ack message is a in JSON format " + "\\\\{" + " status:'COUNT', text:<totalNumberOfMessagesReceived>" + "\\\\}. " +
"Default value is 0, no ack messages will be sent.";
final static String messageAttrDesc =
"Input port's attribute that the data received will be stored to. " +
"If the port has more than one attribute this parameter is required. ";
final static String senderIdAttrDesc =
"Input port attribute that will we loaded with the message sender's identifier, this " +
"identifier is consistent during the lifetime of the sender's session.";
static final String CLASS_NAME="com.ibm.streamsx.inet.wsserver";
private static Logger trace = Logger.getLogger(CLASS_NAME);
private WSServer wsServer;
private int portNum;
private int ackCount;
private String messageAttrName = null;
private String senderIdAttrName = null;
private Metric nMessagesReceived;
private Metric nClientsConnected;
@CustomMetric(description="Number of messages received via WebSocket", kind=Kind.COUNTER)
public void setnMessagesReceived(Metric nMessagesReceived) {
this.nMessagesReceived = nMessagesReceived;
}
public Metric getnMessagesReceived() {
return nMessagesReceived;
}
@CustomMetric(description="Number of clients currently connected to WebSocket port.", kind=Kind.GAUGE)
public void setnClientsConnected(Metric nClientsConnected) {
this.nClientsConnected = nClientsConnected;
}
public Metric getnClientsConnected() {
return nClientsConnected;
}
@Parameter(name="messageAttribute", optional=true,description=messageAttrDesc)
public void setMessageAttrName(String messageAttrName) {
this.messageAttrName = messageAttrName;
}
public String getMessageAttrName() {
return this.messageAttrName;
}
@Parameter(name="senderIdAttribute", optional=true,description=senderIdAttrDesc)
public void setSenderIdAttrName(String senderIdAttrName) {
this.senderIdAttrName = senderIdAttrName;
}
public String getSenerIdAttrName() {
return this.senderIdAttrName;
}
@Parameter(name="ackCount", optional=true,description=parmAckDesc)
public void setAckCount(int ackCount) {
this.ackCount = ackCount;
}
public int ackCount() {
return this.ackCount;
}
// Mandatory port
@Parameter(name="port", optional=false,description=parmPortDesc)
public void setPort(int portNum) {
this.portNum = portNum;
}
public int getPort() {
return this.portNum;
}
/**
* Initialize this operator. Called once before any tuples are processed.
* @param context OperatorContext for this operator.
* @throws Exception Operator failure, will cause the enclosing PE to terminate.
*/
@Override
public synchronized void initialize(OperatorContext context)
throws Exception {
// Must call super.initialize(context) to correctly setup an operator.
super.initialize(context);
// This is important!!! you need this or the Stream is marked dead before
// any data is pushed down.
createAvoidCompletionThread();
trace.log(TraceLevel.INFO,"initalize():Operator " + context.getName() + " initializing in PE: " + context.getPE().getPEId() + " in Job: " + context.getPE().getJobId() );
wsServer = new WSServer(portNum);
wsServer.setAckCount(ackCount);
wsServer.setWebSocketSource(this);
}
@Override
protected void startProcessing() throws Exception {
OperatorContext context = getOperatorContext();
trace.log(TraceLevel.INFO,"startProcessing():ContextName: " + context.getName() + " all ports are ready in PE: " + context.getPE().getPEId() + " in Job: " + context.getPE().getJobId() );
wsServer.start();
}
String attrIdName = null;
String attrMsgTypeName = null, attrMsgName = null;
/**
* Submit new tuples to the output stream
* @throws Exception if an error occurs while submitting a tuple
*
*/
public void produceTuples(String msg, String id) throws Exception {
final StreamingOutput<OutputTuple> out = getOutput(0);
OutputTuple tuple = out.newTuple();
// Set attributes in tuple
if (attrMsgName == null) { // only once ]
if (out.getStreamSchema().getAttributeCount() == 1) { // port only 1 attribute
attrMsgTypeName = out.getStreamSchema().getAttribute(0).getType().getLanguageType();
attrMsgName = out.getStreamSchema().getAttribute(0).getName();
if ((messageAttrName != null) && (messageAttrName != attrMsgName)) {
throw new IllegalArgumentException("Attribute '" + attrMsgName + "' expected, found messageAttribute value '" + messageAttrName +"' - invalid attribute name.");
}
} else { // more than one attribute on port
if (messageAttrName == null) {
throw new IllegalArgumentException("MessageAttribute note specified, must be specified if port has more than 1 attribute.");
}
attrMsgName = messageAttrName;
attrIdName = senderIdAttrName;
if (attrIdName != null) {
if (out.getStreamSchema().getAttribute(attrIdName) == null) {
throw new IllegalArgumentException("No such attribute named '" + attrIdName + "'.");
}
if (out.getStreamSchema().getAttribute(attrIdName).getType().getMetaType() != Type.MetaType.RSTRING) {
String typeString = out.getStreamSchema().getAttribute(attrIdName).getType().getLanguageType();
throw new IllegalArgumentException("Attribute '" + attrIdName + "' type must be 'rstring', found '" + typeString +"'.");
}
}
}
if (out.getStreamSchema().getAttribute(attrMsgName) == null) {
throw new IllegalArgumentException("No such attribute named '" + attrMsgName + "' found.");
}
if (out.getStreamSchema().getAttribute(attrMsgName).getType().getMetaType() != Type.MetaType.RSTRING) {
String typeString = out.getStreamSchema().getAttribute(attrMsgName).getType().getLanguageType();
throw new IllegalArgumentException("Attribute '" + attrMsgName + "' type must be 'rstring', found '" + typeString +"'.");
}
} // done with first time check
if (attrIdName != null) {
tuple.setString(attrIdName, id);
trace.log(TraceLevel.INFO, "produceTuples(): idName:" + attrIdName + " Value:[" + id + "]");
}
tuple.setString(attrMsgName, msg);
if (trace.isEnabledFor(TraceLevel.TRACE)) { trace.log(TraceLevel.TRACE, "produceTuples(): ATTR:" + attrMsgName + " Value:[" + msg + "]"); }
// Submit tuple to output stream
try {
if (trace.isEnabledFor(TraceLevel.TRACE)) { trace.log(TraceLevel.TRACE, "produceTuples():submit id: " + id + " len:" + msg.length());; }
out.submit(tuple);
getnMessagesReceived().incrementValue(1L);
getnClientsConnected().setValue(wsServer.getClientCount());
} catch (Exception iae) {
trace.log(TraceLevel.ERROR, "Failed to submit tuple - msg:" + iae.getLocalizedMessage());
throw(iae);
}
}
/**
* Shutdown this operator, which will interrupt the thread
* executing the <code>produceTuples()</code> method.
* @throws Exception Operator failure, will cause the enclosing PE to terminate.
*/
public synchronized void shutdown() throws Exception {
super.shutdown();
OperatorContext context = getOperatorContext();
trace.log(TraceLevel.INFO,
"shutdown();Operator " + context.getName() +
" shutting down in PE: " + context.getPE().getPEId() +
" in Job: " + context.getPE().getJobId() );
wsServer.stop();
}
}