Java tutorial
/** * Copyright (c) 2016 Bosch Software Innovations GmbH. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bosch Software Innovations GmbH - initial creation * */ package org.eclipse.hono.client.impl; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.qpid.proton.amqp.Binary; import org.apache.qpid.proton.amqp.messaging.Data; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.util.MessageHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonSender; /** * A Vertx-Proton based client for publishing messages to a Hono server. */ abstract class AbstractSender extends AbstractHonoClient implements MessageSender { private static final Logger LOG = LoggerFactory.getLogger(AbstractSender.class); private static final AtomicLong MESSAGE_COUNTER = new AtomicLong(); private static final Pattern CHARSET_PATTERN = Pattern.compile("^.*;charset=(.*)$"); private static final BiConsumer<Object, ProtonDelivery> DEFAULT_DISPOSITION_HANDLER = (messageId, delivery) -> LOG.trace("delivery state updated [message ID: {}, new remote state: {}]", messageId, delivery.getRemoteState()); protected final String tenantId; protected final String targetAddress; private final Handler<String> closeHook; private Handler<Void> drainHandler; private BiConsumer<Object, ProtonDelivery> defaultDispositionHandler = DEFAULT_DISPOSITION_HANDLER; AbstractSender(final ProtonSender sender, final String tenantId, final String targetAddress, final Context context, final Handler<String> closeHook) { super(context); this.sender = Objects.requireNonNull(sender); this.tenantId = Objects.requireNonNull(tenantId); this.targetAddress = targetAddress; this.closeHook = closeHook; } @Override public final int getCredit() { if (sender == null) { return 0; } else { return sender.getCredit(); } } @Override public final boolean sendQueueFull() { return sender.sendQueueFull(); } @Override public final void sendQueueDrainHandler(final Handler<Void> handler) { if (this.drainHandler != null) { throw new IllegalStateException("already waiting for replenishment with credit"); } else { this.drainHandler = Objects.requireNonNull(handler); sender.sendQueueDrainHandler(replenishedSender -> { LOG.trace("sender has received FLOW [credits: {}, queued:{}]", replenishedSender.getCredit(), replenishedSender.getQueued()); final Handler<Void> currentHandler = this.drainHandler; this.drainHandler = null; if (currentHandler != null) { currentHandler.handle(null); } }); } } @Override public final void close(final Handler<AsyncResult<Void>> closeHandler) { Objects.requireNonNull(closeHandler); LOG.info("closing sender ..."); closeLinks(closeHandler); } @Override public final boolean isOpen() { return sender.isOpen(); } @Override public final void setErrorHandler(final Handler<AsyncResult<Void>> errorHandler) { sender.closeHandler(s -> { if (s.failed()) { LOG.debug("server closed link with error condition: {}", s.cause().getMessage()); sender.close(); if (closeHook != null) { closeHook.handle(targetAddress); } errorHandler.handle(Future.failedFuture(s.cause())); } else { LOG.debug("server closed link"); sender.close(); if (closeHook != null) { closeHook.handle(targetAddress); } } }); } @Override public final void setDefaultDispositionHandler(final BiConsumer<Object, ProtonDelivery> dispositionHandler) { this.defaultDispositionHandler = dispositionHandler; } @Override public final void send(final Message rawMessage, final Handler<Void> capacityAvailableHandler, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { Objects.requireNonNull(rawMessage); Objects.requireNonNull(dispositionHandler); if (capacityAvailableHandler == null) { context.runOnContext(send -> { sendMessage(rawMessage, dispositionHandler); }); } else if (this.drainHandler != null) { throw new IllegalStateException("cannot send message while waiting for replenishment with credit"); } else if (sender.isOpen()) { context.runOnContext(send -> { sendMessage(rawMessage, dispositionHandler); if (sender.sendQueueFull()) { sendQueueDrainHandler(capacityAvailableHandler); } else { capacityAvailableHandler.handle(null); } }); } else { throw new IllegalStateException("sender is not open"); } } @Override public final boolean send(final Message rawMessage, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { Objects.requireNonNull(rawMessage); Objects.requireNonNull(dispositionHandler); if (sender.sendQueueFull()) { return false; } else { context.runOnContext(send -> { sendMessage(rawMessage, dispositionHandler); }); return true; } } private void sendMessage(final Message rawMessage, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { sender.send(rawMessage, deliveryUpdated -> dispositionHandler.accept(rawMessage.getMessageId(), deliveryUpdated)); LOG.trace("sent message, remaining credit: {}, queued messages: {}", sender.getCredit(), sender.getQueued()); } @Override public final void send(final Message rawMessage, final Handler<Void> capacityAvailableHandler) { send(rawMessage, capacityAvailableHandler, this.defaultDispositionHandler); } @Override public final boolean send(final Message rawMessage) { return send(rawMessage, this.defaultDispositionHandler); } @Override public final boolean send(final String deviceId, final byte[] payload, final String contentType, final String registrationAssertion) { return send(deviceId, null, payload, contentType, registrationAssertion); } @Override public final boolean send(final String deviceId, final byte[] payload, final String contentType, final String registrationAssertion, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { return send(deviceId, null, payload, contentType, registrationAssertion, dispositionHandler); } @Override public final void send(final String deviceId, final byte[] payload, final String contentType, final String registrationAssertion, final Handler<Void> capacityAvailableHandler) { send(deviceId, null, payload, contentType, registrationAssertion, capacityAvailableHandler); } @Override public final boolean send(final String deviceId, final String payload, final String contentType, final String registrationAssertion) { return send(deviceId, null, payload, contentType, registrationAssertion); } @Override public final boolean send(final String deviceId, final String payload, final String contentType, final String registrationAssertion, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { return send(deviceId, null, payload, contentType, registrationAssertion, dispositionHandler); } @Override public final void send(final String deviceId, final String payload, final String contentType, final String registrationAssertion, final Handler<Void> capacityAvailableHandler) { send(deviceId, null, payload, contentType, registrationAssertion, capacityAvailableHandler); } @Override public final boolean send(final String deviceId, final Map<String, ?> properties, final String payload, final String contentType, final String registrationAssertion) { Objects.requireNonNull(payload); final Charset charset = getCharsetForContentType(Objects.requireNonNull(contentType)); return send(deviceId, properties, payload.getBytes(charset), contentType, registrationAssertion); } @Override public final boolean send(final String deviceId, final Map<String, ?> properties, final String payload, final String contentType, final String registrationAssertion, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { Objects.requireNonNull(payload); final Charset charset = getCharsetForContentType(Objects.requireNonNull(contentType)); return send(deviceId, properties, payload.getBytes(charset), contentType, registrationAssertion, dispositionHandler); } @Override public final boolean send(final String deviceId, final Map<String, ?> properties, final byte[] payload, final String contentType, final String registrationAssertion) { return send(deviceId, properties, payload, contentType, registrationAssertion, this.defaultDispositionHandler); } @Override public final boolean send(final String deviceId, final Map<String, ?> properties, final byte[] payload, final String contentType, final String registrationAssertion, final BiConsumer<Object, ProtonDelivery> dispositionHandler) { Objects.requireNonNull(deviceId); Objects.requireNonNull(payload); Objects.requireNonNull(contentType); Objects.requireNonNull(registrationAssertion); Objects.requireNonNull(dispositionHandler); final Message msg = ProtonHelper.message(); msg.setBody(new Data(new Binary(payload))); setApplicationProperties(msg, properties); addProperties(msg, deviceId, contentType, registrationAssertion); addEndpointSpecificProperties(msg, deviceId); return send(msg, dispositionHandler); } @Override public final void send(final String deviceId, final Map<String, ?> properties, final String payload, final String contentType, final String registrationAssertion, final Handler<Void> capacityAvailableHandler) { send(deviceId, properties, payload, contentType, registrationAssertion, capacityAvailableHandler, defaultDispositionHandler); } @Override public final void send(final String deviceId, final Map<String, ?> properties, final byte[] payload, final String contentType, final String registrationAssertion, final Handler<Void> capacityAvailableHandler) { send(deviceId, properties, payload, contentType, registrationAssertion, capacityAvailableHandler, defaultDispositionHandler); } @Override public void send(String deviceId, Map<String, ?> properties, String payload, String contentType, String registrationAssertion, Handler<Void> capacityAvailableHandler, BiConsumer<Object, ProtonDelivery> dispositionHandler) { Objects.requireNonNull(payload); final Charset charset = getCharsetForContentType(Objects.requireNonNull(contentType)); send(deviceId, properties, payload.getBytes(charset), contentType, registrationAssertion, capacityAvailableHandler, dispositionHandler); } @Override public void send(String deviceId, Map<String, ?> properties, byte[] payload, String contentType, String registrationAssertion, Handler<Void> capacityAvailableHandler, BiConsumer<Object, ProtonDelivery> dispositionHandler) { Objects.requireNonNull(deviceId); Objects.requireNonNull(payload); Objects.requireNonNull(contentType); Objects.requireNonNull(registrationAssertion); final Message msg = ProtonHelper.message(); msg.setAddress(getTo(deviceId)); msg.setBody(new Data(new Binary(payload))); setApplicationProperties(msg, properties); addProperties(msg, deviceId, contentType, registrationAssertion); addEndpointSpecificProperties(msg, deviceId); send(msg, capacityAvailableHandler, dispositionHandler); } /** * Gets the value of the <em>to</em> property to be used for messages produced by this sender. * * @param deviceId The identifier of the device that the message's content originates from. * @return The address. */ protected abstract String getTo(final String deviceId); private void addProperties(final Message msg, final String deviceId, final String contentType, final String registrationAssertion) { msg.setMessageId(String.format("%s-%d", getClass().getSimpleName(), MESSAGE_COUNTER.getAndIncrement())); msg.setContentType(contentType); MessageHelper.addDeviceId(msg, deviceId); MessageHelper.addRegistrationAssertion(msg, registrationAssertion); } /** * Sets additional properties on the message to be sent. * <p> * Subclasses should override this method to set any properties on messages * that are specific to the particular endpoint the message is to be sent to. * <p> * This method does nothing by default. * * @param msg The message to be sent. * @param deviceId The ID of the device that the message's content originates from. */ protected void addEndpointSpecificProperties(final Message msg, final String deviceId) { // empty } private Charset getCharsetForContentType(final String contentType) { final Matcher m = CHARSET_PATTERN.matcher(contentType); if (m.matches()) { return Charset.forName(m.group(1)); } else { return StandardCharsets.UTF_8; } } /** * Creates a sender link. * * @param ctx The vertx context to use for establishing the link. * @param con The connection to create the link for. * @param targetAddress The target address of the link. * @param qos The quality of service to use for the link. * @param closeHook The handler to invoke when the link is closed by the peer. * @return A future for the created link. */ protected static final Future<ProtonSender> createSender(final Context ctx, final ProtonConnection con, final String targetAddress, final ProtonQoS qos, final Handler<String> closeHook) { final Future<ProtonSender> result = Future.future(); ctx.runOnContext(create -> { final ProtonSender sender = con.createSender(targetAddress); sender.setQoS(qos); sender.openHandler(senderOpen -> { if (senderOpen.succeeded()) { LOG.debug("sender open [{}]", sender.getRemoteTarget()); result.complete(senderOpen.result()); } else { LOG.debug("opening sender [{}] failed: {}", targetAddress, senderOpen.cause().getMessage()); result.fail(senderOpen.cause()); } }); sender.closeHandler(senderClosed -> { if (senderClosed.succeeded()) { LOG.debug("sender [{}] closed", targetAddress); } else { LOG.debug("sender [{}] closed: {}", targetAddress, senderClosed.cause().getMessage()); } sender.close(); if (closeHook != null) { closeHook.handle(targetAddress); } }); sender.open(); }); return result; } }