/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.activemq.artemis.core.protocol.mqtt; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException; import org.apache.activemq.artemis.api.core.FilterConstants; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle; import org.apache.activemq.artemis.core.server.BindingQueryResult; import org.apache.activemq.artemis.core.server.Queue; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.core.server.ServerConsumer; import org.apache.activemq.artemis.core.server.impl.AddressInfo; import org.apache.activemq.artemis.utils.CompositeAddress; public class MQTTSubscriptionManager { private MQTTSession session; private ConcurrentMap<Long, Integer> consumerQoSLevels; private ConcurrentMap<String, ServerConsumer> consumers; // We filter out Artemis management messages and notifications private SimpleString managementFilter; public MQTTSubscriptionManager(MQTTSession session) { this.session = session; consumers = new ConcurrentHashMap<>(); consumerQoSLevels = new ConcurrentHashMap<>(); // Create filter string to ignore management messages StringBuilder builder = new StringBuilder(); builder.append("NOT (("); builder.append(FilterConstants.ACTIVEMQ_ADDRESS); builder.append(" = '"); builder.append(session.getServer().getConfiguration().getManagementAddress()); builder.append("') OR ("); builder.append(FilterConstants.ACTIVEMQ_ADDRESS); builder.append(" = '"); builder.append(session.getServer().getConfiguration().getManagementNotificationAddress()); builder.append("'))"); managementFilter = new SimpleString(builder.toString()); } synchronized void start() throws Exception { for (MqttTopicSubscription subscription : session.getSessionState().getSubscriptions()) { String coreAddress = MQTTUtil.convertMQTTAddressFilterToCore(subscription.topicName(), session.getWildcardConfiguration()); Queue q = createQueueForSubscription(coreAddress, subscription.qualityOfService().value()); createConsumerForSubscriptionQueue(q, subscription.topicName(), subscription.qualityOfService().value()); } } synchronized void stop() throws Exception { for (ServerConsumer consumer : consumers.values()) { consumer.setStarted(false); consumer.disconnect(); consumer.getQueue().removeConsumer(consumer); consumer.close(false); } } /** * Creates a Queue if it doesn't already exist, based on a topic and address. Returning the queue name. */ private Queue createQueueForSubscription(String address, int qos) throws Exception { // Check to see if a subscription queue already exists. SimpleString queue = getQueueNameForTopic(address); Queue q = session.getServer().locateQueue(queue); // The queue does not exist so we need to create it. if (q == null) { SimpleString sAddress = SimpleString.toSimpleString(address); // Check we can auto create queues. BindingQueryResult bindingQueryResult = session.getServerSession().executeBindingQuery(sAddress); if (!bindingQueryResult.isAutoCreateQueues()) { throw ActiveMQMessageBundle.BUNDLE.noSuchQueue(sAddress); } // Check that the address exists, if not we try to auto create it. AddressInfo addressInfo = session.getServerSession().getAddress(sAddress); if (addressInfo == null) { if (!bindingQueryResult.isAutoCreateAddresses()) { throw ActiveMQMessageBundle.BUNDLE.addressDoesNotExist(SimpleString.toSimpleString(address)); } addressInfo = session.getServerSession().createAddress(SimpleString.toSimpleString(address), RoutingType.MULTICAST, false); } return findOrCreateQueue(bindingQueryResult, addressInfo, queue, qos); } return q; } private Queue findOrCreateQueue(BindingQueryResult bindingQueryResult, AddressInfo addressInfo, SimpleString queue, int qos) throws Exception { if (addressInfo.getRoutingTypes().contains(RoutingType.MULTICAST)) { return session.getServerSession().createQueue(addressInfo.getName(), queue, RoutingType.MULTICAST, managementFilter, false, MQTTUtil.DURABLE_MESSAGES && qos >= 0, false); } if (addressInfo.getRoutingTypes().contains(RoutingType.ANYCAST)) { if (!bindingQueryResult.getQueueNames().isEmpty()) { SimpleString name = null; for (SimpleString qName : bindingQueryResult.getQueueNames()) { if (name == null) { name = qName; } else if (qName.equals(addressInfo.getName())) { name = qName; } } return session.getServer().locateQueue(name); } else { try { return session.getServerSession().createQueue(addressInfo.getName(), addressInfo.getName(), RoutingType.ANYCAST, managementFilter, false, MQTTUtil.DURABLE_MESSAGES && qos >= 0, false); } catch (ActiveMQQueueExistsException e) { return session.getServer().locateQueue(addressInfo.getName()); } } } Set<RoutingType> routingTypeSet = new HashSet(); routingTypeSet.add(RoutingType.MULTICAST); routingTypeSet.add(RoutingType.ANYCAST); throw ActiveMQMessageBundle.BUNDLE.invalidRoutingTypeForAddress(addressInfo.getRoutingType(), addressInfo.getName().toString(), routingTypeSet); } /** * Creates a new consumer for the queue associated with a subscription */ private void createConsumerForSubscriptionQueue(Queue queue, String topic, int qos) throws Exception { long cid = session.getServer().getStorageManager().generateID(); ServerConsumer consumer = session.getServerSession().createConsumer(cid, queue.getName(), null, false, true, -1); consumer.setStarted(true); consumers.put(topic, consumer); consumerQoSLevels.put(cid, qos); } private void addSubscription(MqttTopicSubscription subscription) throws Exception { String topicName = CompositeAddress.extractAddressName(subscription.topicName()); MqttTopicSubscription s = session.getSessionState().getSubscription(topicName); int qos = subscription.qualityOfService().value(); String coreAddress = MQTTUtil.convertMQTTAddressFilterToCore(topicName, session.getWildcardConfiguration()); session.getSessionState().addSubscription(subscription, session.getWildcardConfiguration()); Queue q = createQueueForSubscription(coreAddress, qos); if (s == null) { createConsumerForSubscriptionQueue(q, topicName, qos); } else { consumerQoSLevels.put(consumers.get(topicName).getID(), qos); } session.getRetainMessageManager().addRetainedMessagesToQueue(q, topicName); } void removeSubscriptions(List<String> topics) throws Exception { for (String topic : topics) { removeSubscription(topic); } } // FIXME: Do we need this synchronzied? private synchronized void removeSubscription(String address) throws Exception { String internalAddress = MQTTUtil.convertMQTTAddressFilterToCore(address, session.getWildcardConfiguration()); SimpleString internalQueueName = getQueueNameForTopic(internalAddress); session.getSessionState().removeSubscription(address); ServerConsumer consumer = consumers.get(address); consumers.remove(address); if (consumer != null) { consumer.removeItself(); consumerQoSLevels.remove(consumer.getID()); } if (session.getServerSession().executeQueueQuery(internalQueueName).isExists()) { session.getServerSession().deleteQueue(internalQueueName); } } private SimpleString getQueueNameForTopic(String topic) { return new SimpleString(session.getSessionState().getClientId() + "." + topic); } /** * As per MQTT Spec. Subscribes this client to a number of MQTT topics. * * @param subscriptions * @return An array of integers representing the list of accepted QoS for each topic. * @throws Exception */ int[] addSubscriptions(List<MqttTopicSubscription> subscriptions) throws Exception { int[] qos = new int[subscriptions.size()]; for (int i = 0; i < subscriptions.size(); i++) { addSubscription(subscriptions.get(i)); qos[i] = subscriptions.get(i).qualityOfService().value(); } return qos; } Map<Long, Integer> getConsumerQoSLevels() { return consumerQoSLevels; } void clean() throws Exception { for (MqttTopicSubscription mqttTopicSubscription : session.getSessionState().getSubscriptions()) { removeSubscription(mqttTopicSubscription.topicName()); } } }