All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.iothub.transport.amqps.AmqpsTransport Maven / Gradle / Ivy

There is a newer version: 1.0.16
Show newest version
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package com.microsoft.azure.iothub.transport.amqps;

import com.microsoft.azure.iothub.*;
import com.microsoft.azure.iothub.Message;
import com.microsoft.azure.iothub.transport.IotHubCallbackPacket;
import com.microsoft.azure.iothub.transport.IotHubOutboundPacket;
import com.microsoft.azure.iothub.transport.IotHubTransport;
import com.microsoft.azure.iothub.transport.State;
import org.apache.qpid.proton.Proton;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.Data;
import org.apache.qpid.proton.amqp.messaging.Properties;
import org.apache.qpid.proton.amqp.messaging.Section;
import org.apache.qpid.proton.message.impl.MessageImpl;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.*;

/**
 * 

* An AMQPS transport. Contains functionality for adding messages and sending * messages to an IoT Hub. Buffers unsent messages until they are received by an * IoT Hub. A transport is bound at construction to the following parameters: * IoT Hub name, device ID and device key. *

* The transport also receives messages from IoT Hub and invokes a * user-defined message callback if a message and callback are found. */ public final class AmqpsTransport implements IotHubTransport, ServerListener { /** The state of the AMQPS transport. */ private State state; /** The {@link AmqpsIotHubConnection} underlying this transport. */ private AmqpsIotHubConnection connection; /** Messages waiting to be sent to the IoT Hub. */ private final Queue waitingMessages = new LinkedBlockingDeque<>(); /** Messages which are sent to the IoT Hub but did not receive ack yet. */ private Map inProgressMessages = new ConcurrentHashMap<>(); /** Messages received from the IoT Hub */ private Queue receivedMessages = new LinkedBlockingQueue<>(); /** Messages whose callbacks that are waiting to be invoked. */ private final Queue callbackList = new LinkedList<>(); private final DeviceClientConfig config; private final Boolean useWebSockets; /** * Constructs an instance from the given {@link DeviceClientConfig} * object. * * @param config configuration parameters for an AMQPS session with an IoT Hub * @param useWebSockets whether the transport should use web sockets or not * */ public AmqpsTransport(final DeviceClientConfig config, Boolean useWebSockets) { // Codes_SRS_AMQPSTRANSPORT_15_001: [The constructor shall save the input parameters into instance variables.] this.config = config; this.useWebSockets = useWebSockets; // Codes_SRS_AMQPSTRANSPORT_15_002: [The constructor shall set the transport state to CLOSED.] this.state = State.CLOSED; } /** * Establishes a communication channel with an IoT Hub. If a channel is * already open, the function shall do nothing. * * @throws IOException if a communication channel cannot be established. */ public synchronized void open() throws IOException { // Codes_SRS_AMQPSTRANSPORT_15_003: [If an AMQPS connection is already open, the function shall do nothing.] if (this.state == State.OPEN) { return; } // Codes_SRS_AMQPSTRANSPORT_15_004: [The function shall open an AMQPS connection with the IoT Hub given in the configuration.] this.connection = new AmqpsIotHubConnection(this.config, this.useWebSockets); try { this.connection.open(); // Codes_SRS_AMQPSTRANSPORT_15_005: [The function shall add the transport to the list of listeners subscribed to the connection events.] this.connection.addListener(this); } catch (Exception e) { throw new IOException(e); } // Codes_SRS_AMQPSTRANSPORT_15_006: [If the connection was opened successfully, the transport state shall be set to OPEN.] this.state = State.OPEN; } /** * Closes all resources used to communicate with an IoT Hub. Once {@code close()} is * called, the transport is no longer usable. If the transport is already * closed, the function shall do nothing. * * @throws IOException if an error occurs in closing the transport. */ public synchronized void close() throws IOException { // Codes_SRS_AMQPSTRANSPORT_15_007: [If the AMQPS connection is closed, the function shall do nothing.] if (this.state == State.CLOSED) { return; } // Codes_SRS_AMQPSTRANSPORT_15_008: [The function shall close an AMQPS connection with the IoT Hub given in the configuration.] this.connection.close(); // Codes_SRS_AMQPSTRANSPORT_15_009: [The function shall set the transport state to CLOSED.] this.state = State.CLOSED; } /** * Adds a message to the transport queue. * * @param message the message to be sent. * @param callback the callback to be invoked when a response for the message is received. * @param callbackContext the context to be passed in when the callback is invoked. * * @throws IllegalStateException if the transport is closed. */ public void addMessage(Message message, IotHubEventCallback callback, Object callbackContext) throws IllegalStateException { // Codes_SRS_AMQPSTRANSPORT_15_010: [If the AMQPS session is closed, the function shall throw an IllegalStateException.] if (this.state == State.CLOSED) { throw new IllegalStateException("Cannot add a message when the AMQPS transport is closed."); } // Codes_SRS_AMQPSTRANSPORT_15_011: [The function shall add a packet containing the message, callback, and callback context to the queue of messages waiting to be sent.] IotHubOutboundPacket packet = new IotHubOutboundPacket(message, callback, callbackContext); this.waitingMessages.add(packet); } /** *

* Sends all messages from the waiting list, one at a time. If a previous * send attempt had failed, the function will attempt to resend the messages * in the previous attempt. *

* * @throws IOException if the server could not be reached. * @throws IllegalStateException if the transport has not been opened or is * already closed. */ public void sendMessages() throws IOException, IllegalStateException { // Codes_SRS_AMQPSTRANSPORT_15_012: [If the AMQPS session is closed, the function shall throw an IllegalStateException.] if (this.state == State.CLOSED) { throw new IllegalStateException("Cannot send messages when the AMQPS transport is closed."); } // Codes_SRS_AMQPSTRANSPORT_15_013: [If there are no messages in the waiting list, the function shall return.] if (this.waitingMessages.size() <= 0) { return; } Collection failedMessages = new ArrayList<>() ; // Codes_SRS_AMQPSTRANSPORT_15_014: [The function shall attempt to send every message on its waiting list, one at a time.] while (!this.waitingMessages.isEmpty()) { IotHubOutboundPacket packet = this.waitingMessages.remove(); Message message = packet.getMessage(); // Codes_SRS_AMQPSTRANSPORT_15_015: [The function shall skip messages with null or empty body.] if (message != null && message.getBytes().length > 0) { // Codes_SRS_AMQPSTRANSPORT_15_039: [If the message is expired, the function shall create a callback // with the MESSAGE_EXPIRED status and add it to the callback list.] if (message.isExpired()) { IotHubCallbackPacket callbackPacket = new IotHubCallbackPacket(IotHubStatusCode.MESSAGE_EXPIRED, packet.getCallback(), packet.getContext()); this.callbackList.add(callbackPacket); } else { // Codes_SRS_AMQPSTRANSPORT_15_036: [The function shall create a new Proton message from the IoTHub message.] MessageImpl protonMessage = iotHubMessageToProtonMessage(message); // Codes_SRS_AMQPSTRANSPORT_15_037: [The function shall attempt to send the Proton message to IoTHub using the underlying AMQPS connection.] Integer sendHash = connection.sendMessage(protonMessage); // Codes_SRS_AMQPSTRANSPORT_15_016: [If the sent message hash is valid, it shall be added to the in progress map.] if (sendHash != -1) { this.inProgressMessages.put(sendHash, packet); } // Codes_SRS_AMQPSTRANSPORT_15_017: [If the sent message hash is not valid, it shall be buffered to be sent in a subsequent attempt.] else { failedMessages.add(packet); } } } } this.waitingMessages.addAll(failedMessages); } /** * Invokes the callbacks for all completed requests. * * @throws IllegalStateException if the transport is closed. */ public void invokeCallbacks() throws IllegalStateException { // Codes_SRS_AMQPSTRANSPORT_15_019: [If the transport closed, the function shall throw an IllegalStateException.] if (this.state == State.CLOSED) { throw new IllegalStateException("Cannot invoke callbacks when AMQPS transport is closed."); } // Codes_SRS_AMQPSTRANSPORT_15_020: [The function shall invoke all the callbacks from the callback queue.] while (!this.callbackList.isEmpty()) { IotHubCallbackPacket packet = this.callbackList.remove(); IotHubStatusCode status = packet.getStatus(); IotHubEventCallback callback = packet.getCallback(); Object context = packet.getContext(); callback.execute(status, context); } } /** *

* Invokes the message callback if a message is found and * responds to the IoT Hub on how the processed message should be * handled by the IoT Hub. *

* If no message callback is set, the function will do nothing. * * @throws IllegalStateException if the transport is closed. */ public void handleMessage() throws IllegalStateException { // Codes_SRS_AMQPSTRANSPORT_15_021: [If the transport is closed, the function shall throw an IllegalStateException.] if (this.state == State.CLOSED) { throw new IllegalStateException("Cannot handle messages when AMQPS transport is closed."); } MessageCallback callback = this.config.getMessageCallback(); // Codes_SRS_AMQPSTRANSPORT_15_025: [If no callback is defined, the list of received messages is cleared.] if (callback == null) { this.receivedMessages.clear(); return; } // Codes_SRS_AMQPSTRANSPORT_15_023: [The function shall attempt to consume a message from the IoT Hub.] // Codes_SRS_AMQPSTRANSPORT_15_024: [If no message was received from IotHub, the function shall return.] if (this.receivedMessages.size() > 0) { AmqpsMessage receivedMessage = this.receivedMessages.remove(); Message message = protonMessageToIoTHubMessage(receivedMessage); // Codes_SRS_AMQPSTRANSPORT_15_026: [The function shall invoke the callback on the message.] IotHubMessageResult result = callback.execute(message, this.config.getMessageContext()); // Codes_SRS_AMQPSTRANSPORT_15_027: [The function shall return the message result (one of COMPLETE, ABANDON, or REJECT) to the IoT Hub.] Boolean ackResult = this.connection.sendMessageResult(receivedMessage, result); // Codes_SRS_AMQPSTRANSPORT_15_028: [If the result could not be sent to IoTHub, the message shall be put back in the received messages queue to be processed again.] if (!ackResult) { receivedMessages.add(receivedMessage); } } } /** * When a message is acknowledged by IoTHub, it is removed from the list of in progress messages and its callback * is added to the list of callbacks to be executed. If the message was not successfully delivered, it is buffered * to be sent again. * @param messageHash The hash of the message. * @param deliveryState The state of the delivery. */ public void messageSent(Integer messageHash, Boolean deliveryState) { // Codes_SRS_AMQPSTRANSPORT_15_029: [If the hash cannot be found in the list of keys for the messages in progress, the method returns.] if (inProgressMessages.containsKey(messageHash)) { IotHubOutboundPacket packet = inProgressMessages.remove(messageHash); if (deliveryState) { // Codes_SRS_AMQPSTRANSPORT_15_030: [If the message was successfully delivered, // its callback is added to the list of callbacks to be executed.] IotHubCallbackPacket callbackPacket = new IotHubCallbackPacket(IotHubStatusCode.OK_EMPTY, packet.getCallback(), packet.getContext()); this.callbackList.add(callbackPacket); } else { // Codes_SRS_AMQPSTRANSPORT_15_031: [If the message was not delivered successfully, it is buffered to be sent again.] waitingMessages.add(packet); } } } /** * If the connection is lost, all the messages in progress are buffered to be sent again. */ public void connectionLost() { // Codes_SRS_AMQPSTRANSPORT_15_032: [The messages in progress are buffered to be sent again.] for (Map.Entry entry : inProgressMessages.entrySet()) { this.waitingMessages.add(entry.getValue()); } // Codes_SRS_AMQPSTRANSPORT_15_033: [The map of messages in progress is cleared.] inProgressMessages.clear(); } /** * When a message is received, it is added to the list of messages to be processed. * @param message The message received. */ public void messageReceived(AmqpsMessage message) { // Codes_SRS_AMQPSTRANSPORT_15_034: [The message received is added to the list of messages to be processed.] this.receivedMessages.add(message); } /** * Returns {@code true} if the transport has no more messages to handle, * and {@code false} otherwise. * * @return {@code true} if the transport has no more messages to handle, * and {@code false} otherwise. */ public boolean isEmpty() { // Codes_SRS_AMQPSTRANSPORT_15_035: [The function shall return true if the waiting list, // in progress list and callback list are all empty, and false otherwise.] return this.waitingMessages.isEmpty() && this.inProgressMessages.size() == 0 && this.callbackList.isEmpty(); } /** * Converts an AMQPS message to a corresponding IoT Hub message. * * @param protonMsg the AMQPS message. * * @return the corresponding IoT Hub message. */ private Message protonMessageToIoTHubMessage(MessageImpl protonMsg) { Data d = (Data) protonMsg.getBody(); Binary b = d.getValue(); byte[] msgBody = new byte[b.getLength()]; ByteBuffer buffer = b.asByteBuffer(); buffer.get(msgBody); Message msg = new Message(msgBody); Properties properties = protonMsg.getProperties(); //Call all of the getters for the Proton message Properties and set those properties //in the IoT Hub message properties if they exist. for (Method m : properties.getClass().getMethods()) { if (m.getName().startsWith("get")) { try { String propertyName = Character.toLowerCase(m.getName().charAt(3)) + m.getName().substring(4); Object value = m.invoke(properties); if (value != null && !propertyName.equals("class")) { String val = value.toString(); if (MessageProperty.isValidAppProperty(propertyName, val)) { msg.setProperty(propertyName, val); } } } catch (IllegalAccessException e) { System.err.println("Attempted to access private or protected member of class during message conversion."); } catch (InvocationTargetException e) { System.err.println("Exception thrown while attempting to get member variable. See: " + e.getMessage()); } } } // Setting the user properties if (protonMsg.getApplicationProperties() != null) { Map applicationProperties = protonMsg.getApplicationProperties().getValue(); for (Map.Entry entry : applicationProperties.entrySet()) { String propertyKey = entry.getKey(); if (!MessageProperty.RESERVED_PROPERTY_NAMES.contains(propertyKey)) { msg.setProperty(entry.getKey(), entry.getValue()); } } } return msg; } /** * Creates a proton message from the IoTHub message. * @param message the IoTHub input message. * @return the proton message. */ private MessageImpl iotHubMessageToProtonMessage(com.microsoft.azure.iothub.Message message) { MessageImpl outgoingMessage = (MessageImpl) Proton.message(); Properties properties = new Properties(); if(message.getMessageId() != null) { properties.setMessageId(message.getMessageId()); } outgoingMessage.setProperties(properties); // Codes_SRS_AMQPSTRANSPORT_15_038: [The function shall add all user properties to the application properties of the Proton message.] if (message.getProperties().length > 0) { Map userProperties = new HashMap<>(message.getProperties().length); for(MessageProperty messageProperty : message.getProperties()) { if (!MessageProperty.RESERVED_PROPERTY_NAMES.contains(messageProperty.getName())) { userProperties.put(messageProperty.getName(), messageProperty.getValue()); } } ApplicationProperties applicationProperties = new ApplicationProperties(userProperties); outgoingMessage.setApplicationProperties(applicationProperties); } Binary binary = new Binary(message.getBytes()); Section section = new Data(binary); outgoingMessage.setBody(section); return outgoingMessage; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy