org.mule.module.pubnub.PubNubModule.java Source code

Java tutorial

Introduction

Here is the source code for org.mule.module.pubnub.PubNubModule.java

Source

/**
 * Mule Pubnub Connector
 *
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.module.pubnub;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;

import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
import org.mule.api.annotations.Configurable;
import org.mule.api.annotations.Module;
import org.mule.api.annotations.Processor;
import org.mule.api.annotations.Source;
import org.mule.api.annotations.lifecycle.Start;
import org.mule.api.annotations.param.Default;
import org.mule.api.annotations.param.Optional;
import org.mule.api.callback.SourceCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

/**
 * PubNub is an Internet-Wide Messaging Service for Real-time Web and Mobile apps and games. PubNub
 * is a massively scalable Real-time Bidirectional Messaging Service in the Cloud.
 * <p/>
 * This connector supports version 3 of the PubNub REST API.
 * <p/>
 * PubNub is a freemium service where users get 5,000 messages per day for free but then have reasonable pricing
 * plans for higher volume usage.  For more information go to PubNub: http://pubnub.com
 *
 * @author MuleSoft, Inc.
 */
@Module(name = "pubnub")
public class PubNubModule {

    public static final int MESSAGE_LIMIT = 1800;
    private static final String ORIGIN = "pubsub.pubnub.com";
    private static Logger logger = LoggerFactory.getLogger(PubNubModule.class);

    private String originUrl;

    /**
     * Your publish key that allows you to send data to the PubNub cloud
     */
    @Configurable
    private String publishKey;

    /**
     * Your subscribe key that allows you to send data to the PubNub cloud
     */
    @Configurable
    private String subscribeKey;

    /**
     * The secret key given to you when you created the account
     */
    @Configurable
    @Optional
    private String secretKey;

    /**
     * Whether to use SSL or not when communicating with the PubNub cloud
     */
    @Configurable
    @Optional
    @Default("false")
    private boolean ssl;

    private Client httpClient = Client.create();
    private ObjectMapper mapper = new ObjectMapper();

    /**
     * Creates a new PubNub connector with no state.  If this constructor is used, then you must
     * also call {@link #config(String, String, String, boolean)}
     */
    public PubNubModule() {
    }

    /**
     * Creates a new PubHub connector with required state. The keys required to create the connector are available from
     * your account on the PubNub website: http://www.pubnub.com/account.
     * SSL can be used by setting the SSL flag.
     *
     * @param publishKey   Your publish key that allows you to send data to the PubNub cloud
     * @param subscribeKey Your subscribe key that allows you to send data to the PubNub cloud
     * @param secretKey    The secret key given to you when you created the account
     * @param ssl          Whether to use SSL or not when communicating with the PubNub cloud
     */
    public PubNubModule(String publishKey, String subscribeKey, String secretKey, boolean ssl) {
        this.publishKey = publishKey;
        this.subscribeKey = subscribeKey;
        this.secretKey = secretKey;
        this.ssl = ssl;
        init();
    }

    /**
     * Creates a new PubHub connector with required state. The keys required to create the connector are available from
     * your account on the PubNub website: http://www.pubnub.com/account.
     *
     * @param publishKey   Your publish key that allows you to send data to the PubNub cloud
     * @param subscribeKey Your subscribe key that allows you to send data to the PubNub cloud
     * @param secretKey    The secret key given to you when you created the account
     */
    public PubNubModule(String publishKey, String subscribeKey, String secretKey) {
        this.publishKey = publishKey;
        this.subscribeKey = subscribeKey;
        this.secretKey = secretKey;
        init();
    }

    public String getPublishKey() {
        return publishKey;
    }

    public void setPublishKey(String publishKey) {
        this.publishKey = publishKey;
    }

    public String getSubscribeKey() {
        return subscribeKey;
    }

    public void setSubscribeKey(String subscribeKey) {
        this.subscribeKey = subscribeKey;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public boolean isSsl() {
        return ssl;
    }

    public void setSsl(boolean ssl) {
        this.ssl = ssl;
    }

    @Start
    public void init() {
        this.originUrl = "http" + (ssl ? "s://" : "://") + ORIGIN;
        httpClient = Client.create();
        mapper = new ObjectMapper();

        httpClient.setReadTimeout(1000 * 60);
        httpClient.setConnectTimeout(1000 * 10);
    }

    protected String getBaseUrl() {
        if (originUrl == null) {
            originUrl = "http" + (ssl ? "s://" : "://") + ORIGIN;
            //Work around because there is no lifecycle support
            httpClient.setReadTimeout(1000 * 60);
            httpClient.setConnectTimeout(1000 * 10);
        }
        return originUrl;
    }

    /**
     * Send a json message to a channel.
     *
     * @param channel     to publish to
     * @param jsonMessage the message to publish
     * @return the response from the server
     */
    public JsonNode publish(String channel, JsonNode jsonMessage) {
        return publish(channel, jsonMessage.toString());
    }

    /**
     * Send a json message to a channel.
     * <p/>
     * {@sample.xml ../../../doc/mule-module-pubnub.xml.sample pubnub:publish}
     *
     * @param channel     name to publish the message to
     * @param jsonMessage the message to publish
     * @return boolean false on fail.
     */
    @Processor
    public JsonNode publish(String channel, String jsonMessage) {
        // Generate String to Sign
        String signature = "0";
        if (StringUtils.isNotBlank(this.secretKey)) {
            StringBuilder string_to_sign = new StringBuilder();
            string_to_sign.append(this.publishKey).append('/').append(this.subscribeKey).append('/')
                    .append(this.secretKey).append('/').append(channel).append('/').append(jsonMessage);

            // Sign Message
            signature = md5(string_to_sign.toString());
        }

        // Build URL
        List<String> url = new ArrayList<String>();
        url.add("publish");
        url.add(this.publishKey);
        url.add(this.subscribeKey);
        url.add(signature);
        url.add(channel);
        url.add("0");
        url.add(jsonMessage);

        // Return JSONArray
        return doRequest(url);
    }

    /**
     * Listen for a message on a channel.
     *
     * {@sample.xml ../../../doc/mule-module-pubnub.xml.sample pubnub:subscribe}
     *
     * @param channel  name to listen on
     * @param callback Callback to execute when a message comes in
     */
    @Source
    public void subscribe(String channel, final SourceCallback callback) {
        this.doSubscribe(channel, new MessageListener() {
            public boolean onMessage(JsonNode message) {
                try {
                    callback.process(message);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
                return true;
            }
        }, "0");
    }

    /**
     * Register a message listener for a channel.  This is a blocking subscribe which also means that only one
     * subscription can be made per connector.
     * <p/>
     * Listen for a message on a channel.
     *
     * @param channel  name to listen on
     * @param listener the message callback to invoke when a message is received. there will be one invocation
     *                 per message and the invocation on the message listener is guaranteed never to be null.
     */
    public void subscribe(String channel, MessageListener listener) {
        this.doSubscribe(channel, listener, "0");
    }

    /**
     * Makes a blocking request to receive a message on a channel.
     * <p/>
     * {@sample.xml ../../../doc/mule-module-pubnub.xml.sample pubnub:request}
     *
     * @param channel name to read from
     * @param timeout how long to wait for a message. The value is expressed in milliseconds. specify for
     * @return zero or more messages depending on what was in the queue
     */
    @Processor
    public JsonNode request(String channel, @Optional @Default("5000") final long timeout) {
        // Build URL
        String timetoken = "0";
        List<String> url = java.util.Arrays.asList("subscribe", this.subscribeKey, channel, "0", timetoken);

        long start = System.currentTimeMillis();
        do {
            // Wait for Message
            JsonNode response = doRequest(url);

            JsonNode messages = response.get(0);
            if (messages.size() > 0) {
                return messages;
            }
            // Update TimeToken
            if (response.get(1).getTextValue().length() > 0) {
                timetoken = response.get(1).getTextValue();
                url = java.util.Arrays.asList("subscribe", this.subscribeKey, channel, "0", timetoken);
            }
        } while ((start + timeout) > System.currentTimeMillis());
        return null;
    }

    /**
     * @param channel   name to subscribe to
     * @param listener  callback for messages received
     * @param timetoken the time since the last read from the server. This value is usually sent with a result
     *                  from the subscribe RESt method on the server.
     */
    private void doSubscribe(String channel, MessageListener listener, String timetoken) {
        while (true) {
            try {
                // Build URL
                List<String> url = java.util.Arrays.asList("subscribe", this.subscribeKey, channel, "0", timetoken);

                // Wait for Message
                JsonNode response = doRequest(url);

                JsonNode messages = response.get(0);

                // Update TimeToken
                if (response.get(1).getTextValue().length() > 0) {
                    timetoken = response.get(1).getTextValue();
                }

                // Run user Callback and Reconnect if user permits. If
                // there's a timeout then messages.length() == 0.
                for (int i = 0; messages.size() > i; i++) {
                    JsonNode message = messages.get(i);
                    if (!listener.onMessage(message)) {
                        return;
                    }
                }
            } catch (Exception e) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

    /**
     * Load history from a channel.
     * <p/>
     * {@sample.xml ../../../doc/mule-module-pubnub.xml.sample pubnub:history}
     *
     * @param channel name.
     * @param limit   history count response.
     * @return JsonNode of history.
     */
    @Processor
    public JsonNode history(String channel, int limit) {
        List<String> url = new ArrayList<String>();

        url.add("history");
        url.add(this.subscribeKey);
        url.add(channel);
        url.add("0");
        url.add(Integer.toString(limit));

        return doRequest(url);
    }

    /**
     * Get the Timestamp from PubNub Cloud.
     * <p/>
     * {@sample.xml ../../../doc/mule-module-pubnub.xml.sample pubnub:server-time}
     *
     * @return double timestamp.
     */
    @Processor
    public double serverTime() {
        List<String> url = new ArrayList<String>();

        url.add("time");
        url.add("0");

        JsonNode response = doRequest(url);
        return response.get(0).getDoubleValue();
    }

    /**
     * Creates a new Json Object message. The {@link org.codehaus.jackson.node.ObjectNode} can be used like a Map
     * to write key/value pairs.  Objects can be text, numeric, binary or complex objects
     *
     * @return a newly created {@link org.codehaus.jackson.node.ObjectNode} instance.
     */
    public ObjectNode createMessage() {
        return mapper.createObjectNode();
    }

    /**
     * Request URL
     *
     * @param urlParts List<String> request of url directories.
     * @return JSONArray from JSON response.
     */
    private JsonNode doRequest(List<String> urlParts) {
        StringBuilder url = new StringBuilder(getBaseUrl());

        // Generate URL with UTF-8 Encoding
        for (final String param : urlParts) {
            try {
                url.append("/").append(_encodeURIcomponent(param));
            } catch (Exception e) {
                return createErrorResponse("Failed UTF-8 Encoding URL");
            }
        }

        // Fail if string too long
        if (url.length() > MESSAGE_LIMIT) {
            return createErrorResponse("Message too long");
        }

        try {
            WebResource webResource = httpClient.resource(url.toString());
            String response = webResource.get(String.class);
            return mapper.readTree(response);
        } catch (Exception e) {
            logger.error("Failed JSONP HTTP Request", e);
            return createErrorResponse("Failed JSONP HTTP Request");
        }
    }

    /**
     * Create a response that represents a failure, explained by the "message".
     * As caller methods may expect a second element representing the timestamp, the 0 default value is added
     * @param message explanation of the failure
     * @return a node representing an Error
     */
    private JsonNode createErrorResponse(String message) {
        final ArrayNode node = mapper.createArrayNode();
        node.add(message);
        node.add("0");
        return node;
    }

    private String _encodeURIcomponent(String s) {
        StringBuilder o = new StringBuilder();
        for (char ch : s.toCharArray()) {
            if (isUnsafe(ch)) {
                o.append('%');
                o.append(toHex(ch / 16));
                o.append(toHex(ch % 16));
            } else {
                o.append(ch);
            }
        }
        return o.toString();
    }

    private char toHex(int ch) {
        return (char) (ch < 10 ? '0' + ch : 'A' + ch - 10);
    }

    private boolean isUnsafe(char ch) {
        return " ~`!@#$%^&*()+=[]\\{}|;':\",./<>?".indexOf(ch) >= 0;
    }

    private String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] messageDigest = md.digest(input.getBytes());
            BigInteger number = new BigInteger(1, messageDigest);
            String hashtext = number.toString(16);

            while (hashtext.length() < 32) {
                hashtext = "0" + hashtext;
            }

            return hashtext;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}