001/*
002# Licensed Materials - Property of IBM
003# Copyright IBM Corp. 2015  
004 */
005package mqtt;
006
007import java.text.SimpleDateFormat;
008import java.util.Date;
009import java.util.HashMap;
010import java.util.Map;
011import java.util.concurrent.Future;
012
013import com.ibm.streamsx.topology.TStream;
014import com.ibm.streamsx.topology.Topology;
015import com.ibm.streamsx.topology.context.ContextProperties;
016import com.ibm.streamsx.topology.context.StreamsContextFactory;
017import com.ibm.streamsx.topology.function.Function;
018import com.ibm.streamsx.topology.function.Supplier;
019import com.ibm.streamsx.topology.function.UnaryOperator;
020import com.ibm.streamsx.topology.logic.Value;
021import com.ibm.streamsx.topology.messaging.mqtt.MqttStreams;
022import com.ibm.streamsx.topology.tuple.Message;
023import com.ibm.streamsx.topology.tuple.SimpleMessage;
024
025
026/**
027 * Demonstrate integrating with the MQTT messaging system
028 * <a href="http://mqtt.org">http://mqtt.org</a>.
029 * <p>
030 * {@link com.ibm.streamsx.topology.messaging.mqtt.MqttStreams MqttStreams} is
031 * a connector used to create a bridge between topology streams
032 * and an MQTT broker.
033 * <p>
034 * The sample publishes some messages to a MQTT topic.  
035 * It also subscribes to the topic and reports the messages received.
036 * The messages received may include messages from prior runs of the sample.
037 * <p>
038 * By default, the sample requires a running MQTT broker with the following
039 * characteristics:
040 * <ul>
041 * <li>the broker's connection is {@code tcp://localhost:1883}</li>
042 * <li>the broker is configured for no authentication</li>
043 * </ul>
044 * <p>
045 * Required IBM Streams environment variables:
046 * <ul>
047 * <li>STREAMS_INSTALL - the Streams installation directory</li>
048 * <li>STREAMS_DOMAIN_ID - the Streams domain to use for context {@code DISTRIBUTED}
049 * <li>STREAMS_INSTANCE_ID - the Streams instance to use for context {@code DISTRIBUTED}
050 * </ul>
051 * <p>
052 * See the MQTT link above for information about setting up a MQTT broker.
053 * <p>
054 * This may be executed from the {@code samples/java/functional} directory as:
055 * <UL>
056 * <LI>{@code ant run.mqtt.distributed} - Using Apache Ant, this will run in distributed mode.</li>
057 * <LI>{@code ant run.mqtt} - Using Apache Ant, this will run in standalone mode.</li>
058 * <LI>
059 * {@code java -cp functionalsamples.jar:../../../com.ibm.streamsx.topology/lib/com.ibm.streamsx.topology.jar:$STREAMS_INSTALL/lib/com.ibm.streams.operator.samples.jar
060 *  mqtt.MqttSample CONTEXT_TYPE
061 *     [serverURI=<value>]
062 *     [userID=<value>] [password=<value>]
063 *     [trustStore=<value>] [trustStorePassword=<value>]
064 *     [keyStore=<value>] [keyStorePassword=<value>]
065 * } - Run directly from the command line.
066 * </LI>
067 * Specify absolute pathnames if using the {@code trustStore}
068 * or {@code keyStore} arguments.
069 * <BR>
070 * <i>CONTEXT_TYPE</i> is one of:
071 * <UL>
072 * <LI>{@code DISTRIBUTED} - Run as an IBM Streams distributed application.</LI>
073 * <LI>{@code STANDALONE} - Run as an IBM Streams standalone application.</LI>
074 * <LI>{@code BUNDLE} - Create an IBM Streams application bundle.</LI>
075 * <LI>{@code TOOLKIT} - Create an IBM Streams application toolkit.</LI>
076 * </UL>
077 * <LI>
078 * An application execution within your IDE once you set the class path to include the correct jars.</LI>
079 * </UL>
080 */
081public class MqttSample {
082    private static String SERVER_URI = "tcp://localhost:1883";    
083    
084    private static final String TOPIC = "mqttSampleTopic";
085
086    private static final int PUB_DELAY_MSEC = 5*1000;
087    private boolean captureArtifacts = false;
088    private boolean setAppTracingLevel = false;
089    private java.util.logging.Level appTracingLevel = java.util.logging.Level.FINE;
090    private static final Map<String,String> authInfo = new HashMap<>();
091    
092    public static void main(String[] args) throws Exception {
093        String contextType = "DISTRIBUTED";
094        if (args.length > 0)
095            contextType = args[0];
096        processArgs(args);
097        System.out.println("\nUsing MQTT broker at " + SERVER_URI +"\n");
098
099        MqttSample app = new MqttSample();
100        app.publishSubscribe(contextType);
101    }
102    
103    /**
104     * Publish some messages to a topic; subscribe to the topic and report
105     * received messages.
106     * @param contextType string value of a {@code StreamsContext.Type}
107     * @throws Exception
108     */
109    public void publishSubscribe(String contextType) throws Exception {
110        
111        Map<String,Object> contextConfig = new HashMap<>();
112        initContextConfig(contextConfig);
113        
114        Topology top = new Topology("mqttSample");
115
116        // A compile time MQTT topic value.
117        Supplier<String> topic = new Value<String>(TOPIC);
118
119        // Create the MQTT connector
120        MqttStreams mqtt = new MqttStreams(top, createMqttConfig());
121        
122        // Create a stream of messages and for the sample, give the
123        // consumer a change to become ready
124        TStream<Message> msgs = makeStreamToPublish(top)
125                        .modify(initialDelayFunc(PUB_DELAY_MSEC));
126
127        // Publish the message stream to the topic
128        mqtt.publish(msgs, topic);
129        
130        // Subscribe to the topic and report received messages
131        TStream<Message> rcvdMsgs = mqtt.subscribe(topic);
132        rcvdMsgs.print();
133
134        // Submit the topology, to send and receive the messages.
135        Future<?> future = StreamsContextFactory.getStreamsContext(contextType)
136                .submit(top, contextConfig);
137        
138        if (contextType.contains("DISTRIBUTED")) {
139            System.out.println("\nSee the job's PE console logs for the topology output.\n"
140                    + "Use Streams Studio or streamtool.  e.g.,\n"
141                    + "    # identify the job's \"Print\" PE\n"
142                    + "    streamtool lspes --jobs " + future.get() + "\n"
143                    + "    # print the PE's console log\n"
144                    + "    streamtool viewlog --print --console --pe <the-peid>"
145                    + "\n");
146            System.out.println("Cancel the job using Streams Studio or streamtool. e.g.,\n"
147                    + "    streamtool canceljob " + future.get()
148                    + "\n");
149        }
150        else if (contextType.contains("STANDALONE")) {
151            Thread.sleep(15000);
152            future.cancel(true);
153        }
154    }
155    
156    private Map<String,Object> createMqttConfig() {
157        Map<String,Object> props = new HashMap<>();
158        props.put("serverURI", SERVER_URI);
159        props.putAll(authInfo);
160        return props;
161    }
162    
163    private static void processArgs(String[] args) {
164        String item = "serverURI";
165        String value = getArg(item, args);
166        if (value != null) {
167            SERVER_URI = value;
168            System.out.println("Using "+item+"="+value);
169        }
170        initAuthInfo("userID", args);
171        initAuthInfo("password", args);
172        if (authInfo.containsKey("password") && !authInfo.containsKey("userID"))
173            authInfo.put("userID", System.getProperty("user.name"));
174        initAuthInfo("trustStore", args);
175        initAuthInfo("trustStorePassword", args);
176        initAuthInfo("keyStore", args);
177        initAuthInfo("keyStorePassword", args);
178    }
179
180    private static void initAuthInfo(String item, String[] args) {
181        String value = getArg(item, args);
182        if (value != null) {
183            authInfo.put(item, value);
184            if (item.toLowerCase().contains("password"))
185                value = "*****";
186            System.out.println("Using "+item+"="+value);
187        }
188    }
189    
190    private static String getArg(String item, String[] args) {
191        for (String arg : args) {
192            String[] parts = arg.split("=");
193            if (item.equals(parts[0]))
194                return parts[1];
195        }
196        return null;
197    }
198    
199    @SuppressWarnings("serial")
200    private static TStream<Message> makeStreamToPublish(Topology top) {
201        return top.strings("Hello", "Are you there?",
202                           "3 of 5", "4 of 5", "5 of 5"
203                ).transform(new Function<String,Message>() {
204                    private String timestamp;
205                    @Override
206                    public Message apply(String v) {
207                        if (timestamp == null)
208                            timestamp = new SimpleDateFormat("HH:mm:ss.SSS ").format(new Date());
209                        return new SimpleMessage(timestamp + v);
210                    }
211                });
212    }
213    
214    private void initContextConfig(Map<String,Object> contextConfig) {
215        if (captureArtifacts)
216            contextConfig.put(ContextProperties.KEEP_ARTIFACTS, true);
217        if (setAppTracingLevel)
218            contextConfig.put(ContextProperties.TRACING_LEVEL, appTracingLevel);
219    }
220
221    @SuppressWarnings("serial")
222    private static UnaryOperator<Message> initialDelayFunc(final int delayMsec) {
223        return new UnaryOperator<Message>() {
224            private int initialDelayMsec = delayMsec;
225    
226            @Override
227            public Message apply(Message v) {
228                if (initialDelayMsec != -1) {
229                    try {
230                        Thread.sleep(initialDelayMsec);
231                    } catch (InterruptedException e) {
232                        // done delaying
233                    }
234                    initialDelayMsec = -1;
235                }
236                return v;
237            }
238        };
239    }
240}