org.carewebframework.vista.mbroker.BrokerSession.java Source code

Java tutorial

Introduction

Here is the source code for org.carewebframework.vista.mbroker.BrokerSession.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
 * If a copy of the MPL was not distributed with this file, You can obtain one at
 * http://mozilla.org/MPL/2.0/.
 *
 * This Source Code Form is also subject to the terms of the Health-Related Additional
 * Disclaimer of Warranty and Limitation of Liability available at
 * http://www.carewebframework.org/licensing/disclaimer.
 */
package org.carewebframework.vista.mbroker;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

import org.apache.commons.lang.StringUtils;

import org.carewebframework.common.JSONUtil;
import org.carewebframework.common.StrUtil;
import org.carewebframework.vista.mbroker.PollingThread.IHostEventHandler;
import org.carewebframework.vista.mbroker.Request.Action;
import org.carewebframework.vista.mbroker.Response.ResponseType;
import org.carewebframework.vista.mbroker.Security.AuthResult;

public class BrokerSession {

    /**
     * Callback interface for asynchronous RPC's.
     */
    public interface IAsyncRPCEvent {

        /**
         * Called when an asynchronous remote procedure request has completed.
         *
         * @param handle Unique handle of the request.
         * @param data Data returned by the request.
         */
        void onRPCComplete(int handle, String data);

        /**
         * Called when an asynchronous remote procedure terminated with an error.
         *
         * @param handle Unique handle of the request.
         * @param code Error code returned by the server.
         * @param text Error text returned by the server.
         */
        void onRPCError(int handle, int code, String text);

    }

    /**
     * Authentication method to be employed.
     */
    public static enum AuthMethod {
        Normal, Cache, NT
    }

    private ConnectionParams connectionParams;

    private ServerCaps serverCaps;

    private ExecutorService executorService;

    private Date hostTime;

    private int id;

    private int userId;

    private byte netSequence;

    private volatile Socket socket;

    private final List<IHostEventHandler> hostEventHandlers = new ArrayList<IHostEventHandler>();

    private PollingThread pollingThread;

    private final List<String> postLoginMessage = new ArrayList<String>();

    private final Map<Integer, IAsyncRPCEvent> callbacks = new HashMap<Integer, IAsyncRPCEvent>();

    public BrokerSession() {

    }

    public BrokerSession(ConnectionParams params) {
        setConnectionParams(params);
    }

    public AuthResult connect() {
        AuthResult authResult = null;

        try {
            close();
            socket = new Socket(connectionParams.getServer(), connectionParams.getPort());
            Request request = new Request(Action.CONNECT);
            request.addParameter("IP", socket.getLocalAddress());
            request.addParameter("LP", socket.getLocalPort());
            request.addParameter("UCI", connectionParams.getNamespace());
            request.addParameter("VER", Constants.VERSION);
            Response response = netCall(request, connectionParams.getTimeout());
            serverCaps = new ServerCaps(response.getData());

            if (!StringUtils.isEmpty(connectionParams.getUsername())
                    && !StringUtils.isEmpty(connectionParams.getPassword())) {
                authResult = authenticate();
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        polling(true);
        return authResult;
    }

    public void disconnect() {
        callbacks.clear();
        polling(false);
        postLoginMessage.clear();
        serverCaps = null;
        userId = 0;

        if (socket != null) {
            Request request = new Request(Action.DISCONNECT);
            request.addParameter("UID", id);

            try {
                netCall(request, 5000);
            } catch (Exception e) {
            }

            close();
        }

        id = 0;
    }

    private void close() {
        polling(false);

        if (socket != null) {
            close(socket);
            socket = null;
            reset();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        close();
        super.finalize();
    }

    private void polling(boolean start) {
        if (start) {
            if (pollingThread == null) {
                pollingThread = new PollingThread(this);
            }
            pollingThread.setEnabled(true);
        } else {
            if (pollingThread != null) {
                pollingThread.terminate();
                pollingThread = null;
            }
        }
    }

    private void reset() {
        id = 0;
        userId = 0;
        netSequence = 0;
    }

    private void close(Socket socket) {
        try {
            socket.close();
        } catch (Exception e) {
        }
    }

    public void ensureConnection() {
        if (!isConnected()) {
            connect();
        }
    }

    public boolean callRPCAbort(int handle) {
        if (getCallback(handle) != null) {
            return callRPCBool("RGNETBAS STOP", handle);
        } else {
            return false;
        }
    }

    public int callRPCAsync(String name, IAsyncRPCEvent callback, Object... args) {
        int handle = StrUtil.toInt(callRPC(name, true, connectionParams.getTimeout(), args));
        callbacks.put(handle, callback);
        return handle;
    }

    public int callRPCInt(String name, Object... args) {
        return StrUtil.toInt(callRPC(name, args));
    }

    public boolean callRPCBool(String name, Object... args) {
        return StrUtil.toBoolean(callRPC(name, args));
    }

    public double callRPCFloat(String name, Object... args) {
        return StrUtil.toDouble(callRPC(name, args));
    }

    public long callRPCLong(String name, Object... args) {
        return StrUtil.toLong(callRPC(name, args));
    }

    public List<String> callRPCList(String name, List<String> list, Object... args) {
        List<String> result = list == null ? new ArrayList<String>() : list;
        StrUtil.toList(callRPC(name, args), result, Constants.LINE_SEPARATOR);
        return result;
    }

    public String callRPC(String name, Object... args) {
        return callRPC(name, false, connectionParams.getTimeout(), args);
    }

    public String callRPC(String name, boolean async, int timeout) {
        return callRPC(name, async, timeout, (RPCParameters) null);
    }

    public String callRPC(String name, boolean async, int timeout, Object... params) {
        return callRPC(name, async, timeout, packageParams(params));
    }

    /**
     * Performs a remote procedure call.
     *
     * @param name Name of the remote procedure. This has the format:
     *            <p>
     *
     *            <pre>
     * &lt;remote procedure name&gt;[:&lt;remote procedure version&gt;][:&lt;calling context&gt;]
     * </pre>
     *            <p>
     *            where only the remote procedure name is required. If the server supports multiple
     *            versions of a remote procedure, an explicit version specifier may be added. If a
     *            different calling context is desired, this may be specified to override the
     *            default. For example:
     *            <p>
     *
     *            <pre>
     * GET LAB RESULTS:2.4:LR CONTEXT
     * </pre>
     * @param async If true, the remote procedure call will be executed asynchronously. In this
     *            case, the value returned by the method will be the unique handle for the
     *            asynchronous request.
     * @param timeout The timeout, in milliseconds, to wait for remote procedure completion.
     * @param params Parameters to be passed to the remote procedure. This may be null.
     * @return The data returned by the remote procedure called if called synchronously, or the
     *         unique handle of the request, if call asynchronously.
     */
    public String callRPC(String name, boolean async, int timeout, RPCParameters params) {
        ensureConnection();
        String version = "";
        String context = connectionParams.getAppid();

        if (name.contains(":")) {
            String pcs[] = StrUtil.split(name, ":", 3, true);
            name = pcs[0];
            version = pcs[1];
            context = pcs[2].isEmpty() ? context : pcs[2];
        }

        Request request = new Request(Action.RPC);
        request.addParameter("UID", id);
        request.addParameter("CTX", context);
        request.addParameter("VER", version);
        request.addParameter("RPC", name);
        request.addParameter("ASY", async);

        if (params != null) {
            request.addParameters(params);
        }

        Response response = netCall(request, timeout);
        return response.getData();
    }

    /**
     * Package parameters for RPC call. If parameters already packaged, simply return the package.
     *
     * @param params Parameters to be packaged.
     * @return Packaged parameters.
     */
    private RPCParameters packageParams(Object... params) {
        if (params == null) {
            return null;
        }

        if (params.length == 1 && params[0] instanceof RPCParameters) {
            return (RPCParameters) params[0];
        }

        return new RPCParameters(params);
    }

    public boolean eventSubscribe(String eventName, boolean subscribe) {
        if (!isConnected()) {
            return false;
        }

        Request request = new Request(subscribe ? Action.SUBSCRIBE : Action.UNSUBSCRIBE);
        request.addParameter("UID", id);
        request.addParameter("EVT", eventName);
        return StrUtil.toBoolean(netCall(request).getData());
    }

    public void fireRemoteEvent(String eventName, Serializable eventData, String recipients) {
        fireRemoteEvent(eventName, eventData, StrUtil.split(recipients, ","));
    }

    public void fireRemoteEvent(String eventName, Serializable eventData, String[] recipients) {
        RPCParameters params = new RPCParameters();
        params.get(0).setValue(eventName);

        RPCParameter param = params.get(1);
        String data = eventData == null ? ""
                : eventData instanceof String ? eventData.toString()
                        : Constants.JSON_PREFIX + JSONUtil.serialize(eventData);
        int index = 0;

        for (int i = 0; i < data.length(); i += 255) {
            param.put(Integer.toString(++index), StringUtils.substring(data, i, i + 255));
        }

        param = params.get(2);

        for (String recip : recipients) {
            if (!recip.isEmpty()) {
                if (processRecipient(param, "#", "UID", recip) || processRecipient(param, "", "DUZ", recip)) {
                }
            }
        }

        callRPC("RGNETBEV BCAST", params);
    }

    private boolean processRecipient(RPCParameter param, String prefix, String subscript, String recipient) {
        if (recipient.startsWith(prefix)) {
            param.put(new String[] { subscript, recipient.substring(prefix.length()) }, "");
            return true;
        }

        return false;
    }

    /**
     * Returns the client's host name. The session must have an active connection.
     *
     * @return Client's host name.
     */
    public String getLocalName() {
        return socket == null ? "" : socket.getLocalAddress().getHostName();
    }

    /**
     * Returns the client's ip address. The session must have an active connection.
     *
     * @return Client's ip address.
     */
    public String getLocalAddress() {
        return socket == null ? "" : socket.getLocalAddress().getHostAddress();
    }

    /**
     * Returns true if the session is currently connected.
     *
     * @return True if the session is currently connected.
     */
    public boolean isConnected() {
        return socket != null && socket.isConnected();
    }

    /**
     * Returns true if the session has been authenticated.
     *
     * @return True if the session has been authenticated.
     */
    public boolean isAuthenticated() {
        return userId != 0;
    }

    /**
     * Request authentication using default settings.
     * 
     * @return Result of authentication.
     */
    public AuthResult authenticate() {
        return authenticate(connectionParams.getUsername(), connectionParams.getPassword(), null);
    }

    /**
     * Request authentication from the server.
     *
     * @param username User name.
     * @param password Password.
     * @param division Login division (may be null).
     * @return Result of authentication.
     */
    public AuthResult authenticate(String username, String password, String division) {
        ensureConnection();

        if (isAuthenticated()) {
            return new AuthResult("0");
        }

        String av = username + ";" + password;
        List<String> results = callRPCList("RGNETBRP AUTH:" + Constants.VERSION, null, connectionParams.getAppid(),
                getLocalName(), "", // This is the pre-authentication token
                ";".equals(av) ? av : Security.encrypt(av, serverCaps.getCipherKey()), getLocalAddress(), division);
        AuthResult authResult = new AuthResult(results.get(0));

        if (authResult.status.succeeded()) {
            setPostLoginMessage(results.subList(2, results.size()));
            init(results.get(1));
        }

        return authResult;
    }

    /**
     * Returns the supported polling action. This is based on the current session state. Possible
     * return values are:
     * <ul>
     * <li>null - No polling is possible (session in disconnected state).</li>
     * <li>Ping - Only ping is supported (updated server params only).</li>
     * <li>Query - Full query is supported (async events and RPCs)</li>
     * </ul>
     *
     * @return Supported polling action.
     */
    public Action pollingAction() {
        return !isConnected() ? null
                : !isAuthenticated() || hostEventHandlers.isEmpty() ? Action.PING : Action.QUERY;
    }

    /**
     * Returns server capabilities descriptor.
     *
     * @return Server capabilities.
     */
    public ServerCaps getServerCaps() {
        return serverCaps;
    }

    /**
     * Returns current connection parameters.
     *
     * @return Connection parameters.
     */
    public ConnectionParams getConnectionParams() {
        return connectionParams;
    }

    /**
     * Sets the connection parameters. Does not affect an existing connection.
     *
     * @param params New connection parameters.
     */
    public void setConnectionParams(ConnectionParams params) {
        this.connectionParams = new ConnectionParams(params);
    }

    /**
     * Flushes the socket's input stream, discarding any pending input.
     */
    private void netFlush() {
        try {
            socket.getInputStream().skip(Long.MAX_VALUE);
        } catch (Exception e) {
        }
    }

    /**
     * Initializes the broker session with information returned by the server.
     *
     * @param init Initialization data.
     */
    protected void init(String init) {
        String[] pcs = StrUtil.split(init, StrUtil.U, 4);
        id = StrUtil.toInt(pcs[0]);
        serverCaps.domainName = pcs[1];
        serverCaps.siteName = pcs[2];
        userId = StrUtil.toInt(pcs[3]);
    }

    /**
     * Issues a request to the server, returning the response. Uses the default timeout set in the
     * connection parameters.
     *
     * @param request Request to be sent.
     * @return Response returned by the server.
     */
    protected synchronized Response netCall(Request request) {
        return netCall(request, connectionParams.getTimeout());
    }

    /**
     * Issues a request to the server, returning the response.
     *
     * @param request Request to be sent.
     * @param timeout The timeout, in milliseconds, to await a response.
     * @return Response returned by the server.
     */
    protected synchronized Response netCall(Request request, int timeout) {
        Response response = null;
        try {
            socket.setSoTimeout(timeout);
            DataOutputStream requestPacket = new DataOutputStream(
                    new BufferedOutputStream(socket.getOutputStream()));
            request.write(requestPacket, nextSequenceId());
            requestPacket.flush();
            DataInputStream responsePacket = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
            response = new Response(responsePacket);

            if (response.getSequenceId() != request.getSequenceId()) {
                throw new IOException("Response is not for current request.");
            }
        } catch (Exception e) {
            netFlush();
            throw new RuntimeException(e);
        }

        if (response.getResponseType() == ResponseType.ERROR) {
            throw new RPCException(response.getData());
        }

        return response;
    }

    /**
     * Returns the next valid sequence #.
     * 
     * @return Sequence #.
     */
    private byte nextSequenceId() {
        while (++netSequence == Constants.EOD) {
        }
        return netSequence;
    }

    /**
     * Returns the current session id.
     *
     * @return The session id.
     */
    public int getId() {
        return id;
    }

    /**
     * Returns the authenticated user id (DUZ).
     *
     * @return The user id.
     */
    public int getUserId() {
        return userId;
    }

    /**
     * Adds an event handler for background polling events.
     *
     * @param hostEventHandler An event handler.
     */
    public void addHostEventHandler(IHostEventHandler hostEventHandler) {
        synchronized (hostEventHandlers) {
            if (!hostEventHandlers.contains(hostEventHandler)) {
                hostEventHandlers.add(hostEventHandler);
            }
        }
    }

    /**
     * Removes an event handler for background polling events.
     *
     * @param hostEventHandler An event handler.
     */
    public void removeHostEventHandler(IHostEventHandler hostEventHandler) {
        synchronized (hostEventHandlers) {
            hostEventHandlers.remove(hostEventHandler);
        }
    }

    /**
     * Returns a list of registered event handlers for background polling events.
     *
     * @return List of registered event handlers.
     */
    protected List<IHostEventHandler> getHostEventHandlers() {
        synchronized (hostEventHandlers) {
            return hostEventHandlers.isEmpty() ? null : new ArrayList<IHostEventHandler>(hostEventHandlers);
        }
    }

    /**
     * Returns the pre-login message text.
     *
     * @return Pre-login message text.
     */
    public List<String> getPreLoginMessage() {
        return serverCaps.getPreLoginMessage();
    }

    /**
     * Returns the post-login message text.
     *
     * @return Post-login message text.
     */
    public List<String> getPostLoginMessage() {
        return postLoginMessage;
    }

    /**
     * Sets the post-login message text.
     *
     * @param message Post-login message text.
     */
    protected void setPostLoginMessage(List<String> message) {
        this.postLoginMessage.clear();
        this.postLoginMessage.addAll(message);
    }

    /**
     * Invokes the callback for the specified handle when an error is encountered during an
     * asynchronous RPC call.
     *
     * @param asyncHandle The unique handle for the asynchronous RPC call.
     * @param asyncError The error code.
     * @param text The error text.
     */
    protected void onRPCError(int asyncHandle, int asyncError, String text) {
        IAsyncRPCEvent callback = getCallback(asyncHandle);

        if (callback != null) {
            callback.onRPCError(asyncHandle, asyncError, text);
        }
    }

    /**
     * Invokes the callback for the specified handle upon successful completion of an asynchronous
     * RPC call.
     *
     * @param asyncHandle The unique handle for the asynchronous RPC call.
     * @param data The data returned by the RPC.
     */
    protected void onRPCComplete(int asyncHandle, String data) {
        IAsyncRPCEvent callback = getCallback(asyncHandle);

        if (callback != null) {
            callback.onRPCComplete(asyncHandle, data);
        }
    }

    /**
     * Returns the asynchronous RPC callback for the specified handle.
     *
     * @param asyncHandle The unique handle for the asynchronous RPC call.
     * @return The callback handler, or null if none found.
     */
    private IAsyncRPCEvent getCallback(int asyncHandle) {
        return callbacks.remove(asyncHandle);
    }

    /**
     * Returns the time as reported by the server.
     *
     * @return The server time.
     */
    public Date getHostTime() {
        return hostTime;
    }

    /**
     * Sets the time reported by the server.
     *
     * @param hostTime The server time.
     */
    protected void setHostTime(Date hostTime) {
        this.hostTime = hostTime;
    }

    /**
     * Returns the executor service used by the session for handling background polling.
     *
     * @return Executor service.
     */
    public ExecutorService getExecutorService() {
        return executorService;
    }

    /**
     * Sets the executor service used by the session for handling background polling.
     *
     * @param executorService Executor service.
     */
    public void setExecutorService(ExecutorService executorService) {
        this.executorService = executorService;
    }

}