/** * Copyright 2015 Santhosh Kumar Tekuri * * The JLibs authors license this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package jlibs.wamp4j.router; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import jlibs.wamp4j.Debugger; import jlibs.wamp4j.Peer; import jlibs.wamp4j.Util; import jlibs.wamp4j.WAMPSerialization; import jlibs.wamp4j.error.ErrorCode; import jlibs.wamp4j.error.InvalidMessageException; import jlibs.wamp4j.msg.*; import jlibs.wamp4j.spi.Listener; import jlibs.wamp4j.spi.MessageType; import jlibs.wamp4j.spi.WAMPOutputStream; import jlibs.wamp4j.spi.WAMPSocket; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import static jlibs.wamp4j.Debugger.AUTOREAD; import static jlibs.wamp4j.Debugger.ROUTER; /** * @author Santhosh Kumar Tekuri */ class Session implements Listener{ private final WAMPRouter router; private final WAMPSocket socket; protected final WAMPSerialization serialization; private Realm realm; long sessionID = -1; // key = registrationID protected final Map<Long, Procedure> procedures = new HashMap<Long, Procedure>(); private long lastRegistrationID = -1; // key = invocationRequestID protected final Map<Long, CallRequest> requests = new HashMap<Long, CallRequest>(); private long lastRequestID = -1; // key = subscriptionID protected final Map<Long, Topic> subscriptions = new HashMap<Long, Topic>(); public Session(WAMPRouter router, WAMPSocket socket, WAMPSerialization serialization){ this.router = router; this.socket = socket; this.serialization = serialization; array = router.array; } private int autoRead = 0; private Map<Long, Session> blockedReaders = new HashMap<Long, Session>(); @Override public void onMessage(WAMPSocket socket, MessageType type, InputStream is){ router.readingSession = this; if(type!=serialization.messageType()){ onError(socket, new RuntimeException("unexpected messageType: " + type)); return; } JsonParser parser = null; try{ parser = serialization.mapper().getFactory().createParser(is); if(parser.nextToken()!=JsonToken.START_ARRAY) throw new InvalidMessageException(); int id = parser.nextIntValue(-1); switch(id){ case HelloMessage.ID: String uri = parser.nextTextValue(); if(uri==null) throw new InvalidMessageException(); realm = router.realms.get(uri); if(ROUTER) Debugger.println(this, "<- HelloMessage: [%d, \"%s\", ...]", id, uri); realm.addSession(this); send(welcomeMessage()); break; case AbortMessage.ID: if(ROUTER) Debugger.println(this, "<- AbortMessage: [%d, ...]", id); socket.close(); break; case GoodbyeMessage.ID: if(ROUTER) Debugger.println(this, "<- GoodbyeMessage: [%d, ...]", id); close(); realm.removeSession(this); parser.close(); break; case RegisterMessage.ID: long requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); if(parser.nextToken()!=JsonToken.START_OBJECT) throw new InvalidMessageException(); ObjectNode options = serialization.mapper().readTree(parser); uri = parser.nextTextValue(); if(ROUTER) Debugger.println(this, "<- RegisterMessage: [%d, %d, %s, \"%s\", ...]", id, requestID, options, uri); if(realm.procedures.containsKey(uri)) send(errorMessage(id, requestID, ErrorCode.procedureAlreadyExists(uri))); else{ RegisterMessage register = new RegisterMessage(requestID, options, uri); RegisteredMessage registered = addProcedure(register); send(registeredMessage(requestID, registered.registrationID)); } break; case UnregisterMessage.ID: requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); long registrationID = parser.nextLongValue(-1); if(registrationID==-1) throw new InvalidMessageException(); Procedure procedure = procedures.remove(registrationID); if(ROUTER) Debugger.println(this, "<- UnregisterMessage: [%d, %d, ...]", id, requestID, registrationID); if(procedure==null) send(errorMessage(id, requestID, ErrorCode.noSuchRegistration(registrationID))); else{ realm.procedures.remove(procedure.register.procedure); // notify waiting callers if any for(Map.Entry<Long, CallRequest> entry : procedure.requests.entrySet()){ requests.remove(entry.getKey()); CallRequest callRequest = entry.getValue(); callRequest.noSuchProcedure(); } send(unregisteredMessage(requestID)); } break; case CallMessage.ID: requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); if(parser.nextToken()!=JsonToken.START_OBJECT) throw new InvalidMessageException(); options = serialization.mapper().readTree(parser); uri = parser.nextTextValue(); if(ROUTER) Debugger.println(this, "<- CallMessage: [%d, %d, %s, \"%s\", ...]", id, requestID, options, uri); procedure = realm.procedures.get(uri); if(procedure==null){ MetaProcedure metaProcedure = MetaProcedures.get(uri); if(metaProcedure==null) send(errorMessage(id, requestID, ErrorCode.noSuchProcedure(uri))); else{ ArrayNode arguments = null; if(parser.nextToken()!=JsonToken.END_ARRAY) arguments = serialization.mapper().readTree(parser); ObjectNode argumentsKw = null; if(parser.nextToken()!=JsonToken.END_ARRAY) argumentsKw = serialization.mapper().readTree(parser); CallMessage call = new CallMessage(requestID, options, uri, arguments, argumentsKw); metaProcedure.reply(this, call); } }else{ long invocationRequestId = procedure.session.addRequest(requestID, this, procedure); procedure.session.send(invocationMessage(invocationRequestId, procedure.registrationID.longValue(), options, parser)); } break; case YieldMessage.ID: requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); if(ROUTER) Debugger.println(this, "<- YieldMessage: [%d, %d, ...]", id, requestID); CallRequest callRequest = requests.remove(requestID); if(callRequest==null) return; callRequest.reply(requestID, parser); break; case ErrorMessage.ID: long requestType = parser.nextLongValue(-1); if(requestType!=InvocationMessage.ID) throw new InvalidMessageException(); requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); if(ROUTER) Debugger.println(this, "<- ErrorMessage: [%d, %d, %d, ...]", id, requestType, requestID); callRequest = requests.remove(requestID); if(callRequest==null) return; callRequest.error(requestID, parser); break; case SubscribeMessage.ID: requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); if(parser.nextToken()!=JsonToken.START_OBJECT) throw new InvalidMessageException(); options = serialization.mapper().readTree(parser); uri = parser.nextTextValue(); if(ROUTER) Debugger.println(this, "<- SubscribeMessage: [%d, %d, %s, \"%s\", ...]", id, requestID, options, uri); long subscriptionID = realm.topics.subscribe(this, uri); send(subscribedMessage(requestID, subscriptionID)); break; case UnsubscribeMessage.ID: requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); subscriptionID = parser.nextLongValue(-1); if(subscriptionID==-1) throw new InvalidMessageException(); if(ROUTER) Debugger.println(this, "<- UnsubscribeMessage: [%d, %d, ...]", id, requestID, subscriptionID); if(realm.topics.unsubscribe(this, subscriptionID)) send(unsubscribedMessage(requestID)); else send(errorMessage(id, requestID, ErrorCode.noSuchSubscription(subscriptionID))); break; case PublishMessage.ID: requestID = parser.nextLongValue(-1); if(requestID==-1) throw new InvalidMessageException(); if(parser.nextToken()!=JsonToken.START_OBJECT) throw new InvalidMessageException(); boolean needsAcknowledgement = needsAcknowledgement(parser); if(ROUTER) Debugger.println(this, "<- PublishMessage: [%d, %d, %s, ...]", id, requestID, needsAcknowledgement); realm.topics.publish(this, null, parser); if(needsAcknowledgement) send(publishedMessage(requestID, 0)); break; default: if(ROUTER) Debugger.println(this, "<- UnknownMessage: [%d, ...]", id); if(id==-1) throw new InvalidMessageException(); } }catch(Throwable thr){ onError(socket, thr); }finally{ try{ if(parser!=null) parser.close(); }catch(Throwable thr){ router.listener.onWarning(router, thr); } } } private boolean needsAcknowledgement(JsonParser parser) throws IOException{ boolean acknowledge = false; boolean readAcknowledge = false; int open = 1; while(true){ JsonToken t = parser.nextToken(); if(readAcknowledge){ readAcknowledge = false; if(t==JsonToken.VALUE_TRUE) acknowledge = true; else if(t==JsonToken.VALUE_FALSE) acknowledge = false; } if(t==null) throw new JsonParseException("unexpected EOF", parser.getCurrentLocation()); else if(t.isStructStart()) open++; else if(t.isStructEnd()){ if(--open==0) return acknowledge; }else if(open==1 && t==JsonToken.FIELD_NAME && parser.getText().equals("acknowledge")){ readAcknowledge = true; } } } @Override public void onReadComplete(WAMPSocket socket){ assert autoRead==0; router.readingSession = null; Session session; while((session=router.removeFromFlushList())!=null){ session.socket.flush(); if(!session.socket.isWritable()){ ++autoRead; session.blockedReaders.put(sessionID, this); } } if(socket.isAutoRead()!=(autoRead==0)){ if(ROUTER && AUTOREAD) Debugger.println(this, "-- autoRead1: "+(autoRead==0)); socket.setAutoRead(autoRead==0); } } @Override public void readyToWrite(WAMPSocket socket){ if(!blockedReaders.isEmpty()){ for(Session session : blockedReaders.values()){ assert session.autoRead>0; if(--session.autoRead==0){ if(ROUTER && AUTOREAD) Debugger.println(session, "-- autoRead2: true"); session.socket.setAutoRead(true); } } blockedReaders.clear(); } } @Override public void onError(WAMPSocket socket, Throwable error){ if(ROUTER) Debugger.println(this, "-- onError: "+error.getMessage()); router.listener.onWarning(router, error); cleanup(); realm.removeSession(this); socket.close(); } @Override public void onClose(WAMPSocket socket){ if(ROUTER) Debugger.println(this, "-- onClose"); assert !socket.isOpen(); if(sessionID!=-1){ cleanup(); realm.removeSession(this); } } private boolean goodbyeSend = false; protected boolean flushNeeded; protected Session flushNext; private final ArrayNode array; protected boolean send(WAMPMessage message){ if(sessionID==-1) return false; if(message instanceof GoodbyeMessage) goodbyeSend = true; WAMPOutputStream out = router.server.createOutputStream(); try{ array.removeAll(); message.toArrayNode(array); serialization.mapper().writeValue(out, array); }catch(Throwable thr){ if(flushNeeded) router.removeFromFlushList(this); router.listener.onError(router, thr); out.release(); cleanup(); realm.removeSession(this); socket.close(); return false; } if(!flushNeeded) router.addToFlushList(this); if(ROUTER) Debugger.println(this, "-> %s", message); socket.send(serialization.messageType(), out); if(!socket.isWritable()){ socket.flush(); if(router.readingSession!=null && !socket.isWritable()){ if(ROUTER && AUTOREAD) Debugger.println(router.readingSession, "-- autoRead3: false"); router.readingSession.socket.setAutoRead(false); } } return true; } protected void send(WAMPOutputStream out){ if(sessionID==-1){ out.release(); return; } if(!flushNeeded) router.addToFlushList(this); if(ROUTER) Debugger.println(this, "%s", Debugger.temp); socket.send(serialization.messageType(), out); if(!socket.isWritable()){ socket.flush(); if(router.readingSession!=null && !socket.isWritable()){ if(ROUTER && AUTOREAD) Debugger.println(router.readingSession, "-- autoRead4: false"); router.readingSession.socket.setAutoRead(false); } } } protected RegisteredMessage addProcedure(RegisterMessage register){ lastRegistrationID = Util.generateID(procedures, lastRegistrationID); Procedure procedure = new Procedure(register, lastRegistrationID, this); procedures.put(lastRegistrationID, procedure); realm.procedures.put(register.procedure, procedure); return new RegisteredMessage(register.requestID, lastRegistrationID); } protected long addRequest(long callID, Session callSession, Procedure procedure){ lastRequestID = Util.generateID(requests, lastRequestID); CallRequest callRequest = new CallRequest(callID, procedure, callSession); requests.put(lastRequestID, callRequest); procedure.requests.put(lastRequestID, callRequest); return lastRequestID; } private void cleanup(){ if(ROUTER) Debugger.println(this, "-- notify waiting callers"); readyToWrite(socket); // wakeup blocked readers if any for(Map.Entry<Long, CallRequest> entry : requests.entrySet()){ CallRequest callRequest = entry.getValue(); callRequest.procedure.requests.remove(entry.getKey()); try{ callRequest.noSuchProcedure(); }catch(Throwable thr){ router.listener.onWarning(router, thr); } } for(Procedure procedure : procedures.values()) realm.procedures.remove(procedure.uri()); while(!subscriptions.isEmpty()) realm.topics.unsubscribe(this, subscriptions.keySet().iterator().next()); } public void close(){ cleanup(); if(!goodbyeSend){ if(send(new GoodbyeMessage("good-bye", ErrorCode.GOODBYE_AND_OUT))) socket.close(); } } public Realm realm(){ return realm; } @Override public String toString(){ return String.format("%s[%s|%d]", getClass().getSimpleName(), realm, sessionID); } protected WAMPOutputStream numbers(int id, long number1) throws Throwable{ WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(id); json.writeNumber(number1); json.writeEndArray(); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream numbers(int id, long number1, long number2) throws Throwable{ WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(id); json.writeNumber(number1); json.writeNumber(number2); json.writeEndArray(); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream welcomeMessage() throws Throwable{ if(ROUTER) Debugger.temp("<- WelcomeMessage: [%d, %d, %s]", WelcomeMessage.ID, sessionID, Peer.router.details); WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(WelcomeMessage.ID); json.writeNumber(sessionID); json.writeTree(Peer.router.details); json.writeEndArray(); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream registeredMessage(long requestID, long registrationID) throws Throwable{ if(ROUTER) Debugger.temp("<- RegisteredMessage: [%d, %d, %d,...]", RegisteredMessage.ID, requestID, registrationID); return numbers(RegisteredMessage.ID, requestID, registrationID); } protected WAMPOutputStream unregisteredMessage(long requestID) throws Throwable{ if(ROUTER) Debugger.temp("<- UnregisteredMessage: [%d, %d, ...]", UnregisteredMessage.ID, requestID); return numbers(UnregisteredMessage.ID, requestID); } protected WAMPOutputStream invocationMessage(long requestID, long registrationID, ObjectNode details, JsonParser call) throws Throwable{ if(ROUTER) Debugger.temp("<- InvocationMessage: [%d, %d, %s, ...]", InvocationMessage.ID, requestID, registrationID, details); WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(InvocationMessage.ID); json.writeNumber(requestID); json.writeNumber(registrationID); if(details==null){ json.writeStartObject(); json.writeEndObject(); }else json.writeTree(details); while(call.nextToken()!=null) json.copyCurrentEvent(call); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream resultMessage(long requestID, JsonParser yield) throws Throwable{ if(ROUTER) Debugger.temp("<- ResultMessage: [%d, %d, ...]", ResultMessage.ID, requestID); WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(ResultMessage.ID); json.writeNumber(requestID); while(yield.nextToken()!=null) json.copyCurrentEvent(yield); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream errorMessage(int requestType, long requestID, ErrorCode errorCode) throws Throwable{ if(ROUTER) Debugger.temp("<- ErrorMessage: [%d, %d, %d, {}, \"%s\", %s, %s]", ErrorMessage.ID, requestType, requestID, errorCode.uri, errorCode.arguments, errorCode.argumentsKw); WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(ErrorMessage.ID); json.writeNumber(requestType); json.writeNumber(requestID); json.writeStartObject(); json.writeEndObject(); json.writeString(errorCode.uri); json.writeTree(errorCode.arguments); json.writeTree(errorCode.argumentsKw); json.writeEndArray(); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream errorMessage(int requestType, long requestID, JsonParser error) throws Throwable{ if(ROUTER) Debugger.temp("<- ErrorMessage: [%d, %d, %d, ...]", ErrorMessage.ID, requestType, requestID); WAMPOutputStream out = router.server.createOutputStream(); try{ JsonGenerator json = serialization.mapper().getFactory().createGenerator(out); json.writeStartArray(); json.writeNumber(ErrorMessage.ID); json.writeNumber(requestType); json.writeNumber(requestID); while(error.nextToken()!=null) json.copyCurrentEvent(error); json.close(); return out; }catch(Throwable thr){ out.release(); throw thr; } } protected WAMPOutputStream subscribedMessage(long requestID, long subscriptionID) throws Throwable{ if(ROUTER) Debugger.temp("<- SubscribedMessage: [%d, %d, %d, ...]", SubscribedMessage.ID, requestID, subscriptionID); return numbers(SubscribedMessage.ID, requestID, subscriptionID); } protected WAMPOutputStream unsubscribedMessage(long requestID) throws Throwable{ if(ROUTER) Debugger.temp("<- UnsubscribedMessage: [%d, %d, ...]", UnsubscribedMessage.ID, requestID); return numbers(UnsubscribedMessage.ID, requestID); } protected WAMPOutputStream publishedMessage(long requestID, long publicationID) throws Throwable{ if(ROUTER) Debugger.temp("<- PublishedMessage: [%d, %d, %d, ...]", PublishedMessage.ID, requestID, publicationID); return numbers(PublishedMessage.ID, requestID, publicationID); } }