/****************************************************************************** * * Copyright 2011-2012 Tavendo GmbH * * Licensed 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 org.magnum.soda.transport.wamp; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import android.os.HandlerThread; import android.util.Log; import com.fasterxml.jackson.core.type.TypeReference; public class WampConnection extends WebSocketConnection implements Wamp { private static final boolean DEBUG = true; private static final String TAG = WampConnection.class.getName(); /// The message handler of the background writer. protected WampWriter mWriterHandler; /// Prefix map for outgoing messages. private final PrefixMap mOutgoingPrefixes = new PrefixMap(); /// RNG for IDs. private final Random mRng = new Random(); /// Set of chars to be used for IDs. private static final char[] mBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" .toCharArray(); /** * RPC metadata. */ public static class CallMeta { CallMeta(CallHandler handler, Class<?> resultClass) { this.mResultHandler = handler; this.mResultClass = resultClass; this.mResultTypeRef = null; } CallMeta(CallHandler handler, TypeReference<?> resultTypeReference) { this.mResultHandler = handler; this.mResultClass = null; this.mResultTypeRef = resultTypeReference; } /// Call handler to be fired on. public CallHandler mResultHandler; /// Desired call result type or null. public Class<?> mResultClass; /// Desired call result type or null. public TypeReference<?> mResultTypeRef; } /// Metadata about issued, but not yet returned RPCs. private final ConcurrentHashMap<String, CallMeta> mCalls = new ConcurrentHashMap<String, CallMeta>(); /** * Event subscription metadata. */ public static class SubMeta { SubMeta(EventHandler handler, Class<?> resultClass) { this.mEventHandler = handler; this.mEventClass = resultClass; this.mEventTypeRef = null; } SubMeta(EventHandler handler, TypeReference<?> resultTypeReference) { this.mEventHandler = handler; this.mEventClass = null; this.mEventTypeRef = resultTypeReference; } /// Event handler to be fired on. public EventHandler mEventHandler; /// Desired event type or null. public Class<?> mEventClass; /// Desired event type or null. public TypeReference<?> mEventTypeRef; } /// Metadata about active event subscriptions. private final ConcurrentHashMap<String, SubMeta> mSubs = new ConcurrentHashMap<String, SubMeta>(); /// The session handler provided to connect(). private Wamp.ConnectionHandler mSessionHandler; /** * Create the connection transmitting leg writer. */ protected void createWriter() { mWriterThread = new HandlerThread("AutobahnWriter"); mWriterThread.start(); mWriter = new WampWriter(mWriterThread.getLooper(), mMasterHandler, mTransportChannel, mOptions); if (DEBUG) Log.d(TAG, "writer created and started"); } /** * Create the connection receiving leg reader. */ protected void createReader() { mReader = new WampReader(mCalls, mSubs, mMasterHandler, mTransportChannel, mOptions, "AutobahnReader"); mReader.start(); if (DEBUG) Log.d(TAG, "reader created and started"); } /** * Create new random ID. This is used, i.e. for use in RPC calls to correlate * call message with result message. * * @param len Length of ID. * @return New random ID of given length. */ private String newId(int len) { char[] buffer = new char[len]; for (int i = 0; i < len; i++) { buffer[i] = mBase64Chars[mRng.nextInt(mBase64Chars.length)]; } return new String(buffer); } /** * Create new random ID of default length. * * @return New random ID of default length. */ private String newId() { return newId(8); } public void connect(String wsUri, Wamp.ConnectionHandler sessionHandler) { WampOptions options = new WampOptions(); options.setReceiveTextMessagesRaw(true); options.setMaxMessagePayloadSize(64*1024); options.setMaxFramePayloadSize(64*1024); options.setTcpNoDelay(true); connect(wsUri, sessionHandler, options); } /** * Connect to server. * * @param wsUri WebSockets server URI. * @param sessionHandler The session handler to fire callbacks on. */ public void connect(String wsUri, Wamp.ConnectionHandler sessionHandler, WampOptions options) { mSessionHandler = sessionHandler; mCalls.clear(); mSubs.clear(); mOutgoingPrefixes.clear(); try { connect(wsUri, new String[] {"wamp"}, new WebSocketConnectionHandler() { @Override public void onOpen() { if (mSessionHandler != null) { mSessionHandler.onOpen(); } else { if (DEBUG) Log.d(TAG, "could not call onOpen() .. handler already NULL"); } } @Override public void onClose(int code, String reason) { if (mSessionHandler != null) { mSessionHandler.onClose(code, reason); } else { if (DEBUG) Log.d(TAG, "could not call onClose() .. handler already NULL"); } } }, options); } catch (WebSocketException e) { if (mSessionHandler != null) { mSessionHandler.onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT, "cannot connect (" + e.toString() + ")"); } else { if (DEBUG) Log.d(TAG, "could not call onClose() .. handler already NULL"); } } } /** * Process WAMP messages coming from the background reader. */ protected void processAppMessage(Object message) { if (message instanceof WampMessage.CallResult) { WampMessage.CallResult callresult = (WampMessage.CallResult) message; if (mCalls.containsKey(callresult.mCallId)) { CallMeta meta = mCalls.get(callresult.mCallId); if (meta.mResultHandler != null) { meta.mResultHandler.onResult(callresult.mResult); } mCalls.remove(callresult.mCallId); } } else if (message instanceof WampMessage.CallError) { WampMessage.CallError callerror = (WampMessage.CallError) message; if (mCalls.containsKey(callerror.mCallId)) { CallMeta meta = mCalls.get(callerror.mCallId); if (meta.mResultHandler != null) { meta.mResultHandler.onError(callerror.mErrorUri, callerror.mErrorDesc); } mCalls.remove(callerror.mCallId); } } else if (message instanceof WampMessage.Event) { WampMessage.Event event = (WampMessage.Event) message; if (mSubs.containsKey(event.mTopicUri)) { SubMeta meta = mSubs.get(event.mTopicUri); if (meta != null && meta.mEventHandler != null) { meta.mEventHandler.onEvent(event.mTopicUri, event.mEvent); } } } else if (message instanceof WampMessage.Welcome) { WampMessage.Welcome welcome = (WampMessage.Welcome) message; // FIXME: safe session ID / fire session opened hook if (DEBUG) Log.d(TAG, "WAMP session " + welcome.mSessionId + " established (protocol version " + welcome.mProtocolVersion + ", server " + welcome.mServerIdent + ")"); } else { if (DEBUG) Log.d(TAG, "unknown WAMP message in AutobahnConnection.processAppMessage"); } } /** * Issue a remote procedure call (RPC). * * @param procUri URI or CURIE of procedure to call. * @param resultMeta Call result metadata. * @param arguments Call arguments. */ private void call(String procUri, CallMeta resultMeta, Object... arguments) { WampMessage.Call call = new WampMessage.Call(newId(), procUri, arguments.length); for (int i = 0; i < arguments.length; ++i) { call.mArgs[i] = arguments[i]; } mWriter.forward(call); mCalls.put(call.mCallId, resultMeta); } /** * Issue a remote procedure call (RPC). This version should be used with * primitive Java types and simple composite (class) types. * * @param procUri URI or CURIE of procedure to call. * @param resultType Type we want the call result to be converted to. * @param resultHandler Call handler to process call result or error. * @param arguments Call arguments. */ public void call(String procUri, Class<?> resultType, CallHandler resultHandler, Object... arguments) { call(procUri, new CallMeta(resultHandler, resultType), arguments); } /** * Issue a remote procedure call (RPC). This version should be used with * result types which are containers, i.e. List<> or Map<>. * * @param procUri URI or CURIE of procedure to call. * @param resultType Type we want the call result to be converted to. * @param resultHandler Call handler to process call result or error. * @param arguments Call arguments. */ public void call(String procUri, TypeReference<?> resultType, CallHandler resultHandler, Object... arguments) { call(procUri, new CallMeta(resultHandler, resultType), arguments); } /** * Subscribe to topic to receive events for. * * @param topicUri URI or CURIE of topic to subscribe to. * @param meta Subscription metadata. */ private void subscribe(String topicUri, SubMeta meta) { String uri = mOutgoingPrefixes.resolveOrPass(topicUri); if (!mSubs.containsKey(uri)) { WampMessage.Subscribe msg = new WampMessage.Subscribe(mOutgoingPrefixes.shrink(topicUri)); mWriter.forward(msg); } mSubs.put(uri, meta); } /** * Subscribe to topic to receive events for. This version should be used with * result types which are containers, i.e. List<> or Map<>. * * @param topicUri URI or CURIE of topic to subscribe to. * @param eventType The type we want events to be converted to. * @param eventHandler The event handler to process received events. */ public void subscribe(String topicUri, Class<?> eventType, EventHandler eventHandler) { subscribe(topicUri, new SubMeta(eventHandler, eventType)); } /** * Subscribe to topic to receive events for. This version should be used with * primitive Java types and simple composite (class) types. * * @param topicUri URI or CURIE of topic to subscribe to. * @param eventType The type we want events to be converted to. * @param eventHandler The event handler to process received events. */ public void subscribe(String topicUri, TypeReference<?> eventType, EventHandler eventHandler) { subscribe(topicUri, new SubMeta(eventHandler, eventType)); } /** * Unsubscribe from topic. * * @param topicUri URI or CURIE of topic to unsubscribe from. */ public void unsubscribe(String topicUri) { if (mSubs.containsKey(topicUri)) { WampMessage.Unsubscribe msg = new WampMessage.Unsubscribe(topicUri); mWriter.forward(msg); } } /** * Unsubscribe from any subscribed topic. */ public void unsubscribe() { for (String topicUri : mSubs.keySet()) { WampMessage.Unsubscribe msg = new WampMessage.Unsubscribe(topicUri); mWriter.forward(msg); } } /** * Establish a prefix to be used in CURIEs. * * @param prefix The prefix to be used in CURIEs. * @param uri The full URI this prefix shall resolve to. */ public void prefix(String prefix, String uri) { String currUri = mOutgoingPrefixes.get(prefix); if (currUri == null || !currUri.equals(uri)) { mOutgoingPrefixes.set(prefix, uri); WampMessage.Prefix msg = new WampMessage.Prefix(prefix, uri); mWriter.forward(msg); } } /** * Publish an event to a topic. * * @param topicUri URI or CURIE of topic to publish event on. * @param event Event to be published. */ public void publish(String topicUri, Object event) { WampMessage.Publish msg = new WampMessage.Publish(mOutgoingPrefixes.shrink(topicUri), event); mWriter.forward(msg); } }