Moved packages from old io.moquette.server to new io.moquette.broker

This commit is contained in:
andsel
2018-11-11 18:06:13 +01:00
parent 58fd90d33d
commit 1ba9eb3613
127 changed files with 274 additions and 1414 deletions
@@ -1,132 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.netty.handler.codec.mqtt.MqttQoS;
import java.util.*;
class CNode {
Token token;
private List<INode> children;
Set<Subscription> subscriptions;
CNode() {
this.children = new ArrayList<>();
this.subscriptions = new HashSet<>();
}
//Copy constructor
private CNode(Token token, List<INode> children, Set<Subscription> subscriptions) {
this.token = token; // keep reference, root comparison in directory logic relies on it for now.
this.subscriptions = new HashSet<>(subscriptions);
this.children = new ArrayList<>(children);
}
boolean anyChildrenMatch(Token token) {
for (INode iNode : children) {
final CNode child = iNode.mainNode();
if (child.equalsToken(token)) {
return true;
}
}
return false;
}
List<INode> allChildren() {
return this.children;
}
INode childOf(Token token) {
for (INode iNode : children) {
final CNode child = iNode.mainNode();
if (child.equalsToken(token)) {
return iNode;
}
}
throw new IllegalArgumentException("Asked for a token that doesn't exists in any child [" + token + "]");
}
private boolean equalsToken(Token token) {
return token != null && this.token != null && this.token.equals(token);
}
@Override
public int hashCode() {
return Objects.hash(token);
}
CNode copy() {
return new CNode(this.token, this.children, this.subscriptions);
}
public void add(INode newINode) {
this.children.add(newINode);
}
public void remove(INode node) {
this.children.remove(node);
}
CNode addSubscription(Subscription newSubscription) {
// if already contains one with same topic and same client, keep that with higher QoS
if (subscriptions.contains(newSubscription)) {
final Subscription existing = subscriptions.stream()
.filter(s -> s.equals(newSubscription))
.findFirst().get();
if (existing.getRequestedQos().value() < newSubscription.getRequestedQos().value()) {
subscriptions.remove(existing);
subscriptions.add(new Subscription(newSubscription));
}
} else {
this.subscriptions.add(new Subscription(newSubscription));
}
return this;
}
/**
* @return true iff the subscriptions contained in this node are owned by clientId
* AND at least one subscription is actually present for that clientId
* */
boolean containsOnly(String clientId) {
for (Subscription sub : this.subscriptions) {
if (!sub.clientId.equals(clientId)) {
return false;
}
}
return !this.subscriptions.isEmpty();
}
//TODO this is equivalent to negate(containsOnly(clientId))
public boolean contains(String clientId) {
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
return true;
}
}
return false;
}
void removeSubscriptionsFor(String clientId) {
Set<Subscription> toRemove = new HashSet<>();
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
toRemove.add(sub);
}
}
this.subscriptions.removeAll(toRemove);
}
}
@@ -1,296 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.broker.ISubscriptionsRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
private static final Logger LOG = LoggerFactory.getLogger(CTrieSubscriptionDirectory.class);
private static final Token ROOT = new Token("root");
private static final INode NO_PARENT = null;
INode root;
private volatile ISubscriptionsRepository sessionsRepository;
interface IVisitor<T> {
void visit(CNode node, int deep);
T getResult();
}
private enum Action {
OK, REPEAT
}
@Override
public void init(ISubscriptionsRepository sessionsRepository) {
LOG.info("Initializing CTrie");
final CNode mainNode = new CNode();
mainNode.token = ROOT;
this.root = new INode(mainNode);
LOG.info("Initializing subscriptions store...");
this.sessionsRepository = sessionsRepository;
// reload any subscriptions persisted
if (LOG.isTraceEnabled()) {
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
}
for (Subscription subscription : this.sessionsRepository.listAllSubscriptions()) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", subscription.clientId,
subscription.topicFilter);
add(subscription);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
}
}
Optional<CNode> lookup(Topic topic) {
INode inode = this.root;
Token token = topic.headToken();
while (!topic.isEmpty() && (inode.mainNode().anyChildrenMatch(token))) {
topic = topic.exceptHeadToken();
inode = inode.mainNode().childOf(token);
token = topic.headToken();
}
if (inode == null || !topic.isEmpty()) {
return Optional.empty();
}
return Optional.of(inode.mainNode());
}
@Override
public List<Subscription> matches(Topic topic) {
return new ArrayList<>(matchQosSharpening(topic));
}
/**
* Given a topic string return the clients subscriptions that matches it. Topic string can't
* contain character # and + because they are reserved to listeners subscriptions, and not topic
* publishing.
*
* @param topic
* to use fo searching matching subscriptions.
* @return the list of matching subscriptions, or empty if not matching.
*/
@Override
public Set<Subscription> matchWithoutQosSharpening(Topic topic) {
return recursiveMatch(topic, this.root);
}
@Override
public Set<Subscription> matchQosSharpening(Topic topic) {
final Set<Subscription> subscriptions = matchWithoutQosSharpening(topic);
Map<String, Subscription> subsGroupedByClient = new HashMap<>();
for (Subscription sub : subscriptions) {
Subscription existingSub = subsGroupedByClient.get(sub.clientId);
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.qosLessThan(sub)) {
subsGroupedByClient.put(sub.clientId, sub);
}
}
return new HashSet<>(subsGroupedByClient.values());
}
Set<Subscription> recursiveMatch(Topic topic, INode inode) {
CNode cnode = inode.mainNode();
if (Token.MULTI.equals(cnode.token)) {
return cnode.subscriptions;
}
if (topic.isEmpty()) {
return Collections.emptySet();
}
if (cnode instanceof TNode) {
return Collections.emptySet();
}
final Token token = topic.headToken();
if (!(Token.SINGLE.equals(cnode.token) || cnode.token.equals(token) || ROOT.equals(cnode.token))) {
return Collections.emptySet();
}
Topic remainingTopic = (ROOT.equals(cnode.token)) ? topic : topic.exceptHeadToken();
Set<Subscription> subscriptions = new HashSet<>();
if (remainingTopic.isEmpty()) {
subscriptions.addAll(cnode.subscriptions);
}
for (INode subInode : cnode.allChildren()) {
subscriptions.addAll(recursiveMatch(remainingTopic, subInode));
}
return subscriptions;
}
/**
*
* Cleans Disposes of TNode in separate Atomic CAS operation per
* http://bravenewgeek.com/breaking-and-entering-lose-the-lock-while-embracing-concurrency/
*
* We roughly follow this theory above, but we allow CNode with no Subscriptions to linger (for now).
*
*
* @param inode inode that handle to the tomb node.
* @param iParent inode parent.
* @return REPEAT if the this methods wasn't successful or OK.
*/
public Action cleanTomb(INode inode, INode iParent) {
CNode updatedCnode = iParent.mainNode().copy();
updatedCnode.remove(inode);
return iParent.compareAndSet(iParent.mainNode(), updatedCnode) ? Action.OK : Action.REPEAT;
}
@Override
public void add(Subscription newSubscription) {
Action res;
do {
res = insert(newSubscription.topicFilter, this.root, newSubscription);
} while (res == Action.REPEAT);
}
private Action insert(Topic topic, final INode inode, Subscription newSubscription) {
Token token = topic.headToken();
if (!topic.isEmpty() && inode.mainNode().anyChildrenMatch(token)) {
Topic remainingTopic = topic.exceptHeadToken();
INode nextInode = inode.mainNode().childOf(token);
return insert(remainingTopic, nextInode, newSubscription);
} else {
if (topic.isEmpty()) {
return insertSubscription(inode, newSubscription);
} else {
return createNodeAndInsertSubscription(topic, inode, newSubscription);
}
}
}
private Action insertSubscription(INode inode, Subscription newSubscription) {
CNode cnode = inode.mainNode();
CNode updatedCnode = cnode.copy().addSubscription(newSubscription);
if (inode.compareAndSet(cnode, updatedCnode)) {
return Action.OK;
} else {
return Action.REPEAT;
}
}
private Action createNodeAndInsertSubscription(Topic topic, INode inode, Subscription newSubscription) {
INode newInode = createPathRec(topic, newSubscription);
CNode cnode = inode.mainNode();
CNode updatedCnode = cnode.copy();
updatedCnode.add(newInode);
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT;
}
private INode createLeafNodes(Token token, Subscription newSubscription) {
CNode newLeafCnode = new CNode();
newLeafCnode.token = token;
newLeafCnode.addSubscription(newSubscription);
return new INode(newLeafCnode);
}
private INode createPathRec(Topic topic, Subscription newSubscription) {
Topic remainingTopic = topic.exceptHeadToken();
if (!remainingTopic.isEmpty()) {
INode inode = createPathRec(remainingTopic, newSubscription);
CNode cnode = new CNode();
cnode.token = topic.headToken();
cnode.add(inode);
return new INode(cnode);
} else {
return createLeafNodes(topic.headToken(), newSubscription);
}
}
/**
* Removes subscription from CTrie, adds TNode when the last client unsubscribes, then calls for cleanTomb in a
* separate atomic CAS operation.
*
* @param topic the subscription's topic to remove.
* @param clientID the Id of client owning the subscription.
*/
@Override
public void removeSubscription(Topic topic, String clientID) {
Action res;
do {
res = remove(clientID, topic, this.root, NO_PARENT);
} while (res == Action.REPEAT);
}
private Action remove(String clientId, Topic topic, INode inode, INode iParent) {
Token token = topic.headToken();
if (!topic.isEmpty() && (inode.mainNode().anyChildrenMatch(token))) {
Topic remainingTopic = topic.exceptHeadToken();
INode nextInode = inode.mainNode().childOf(token);
return remove(clientId, remainingTopic, nextInode, inode);
} else {
final CNode cnode = inode.mainNode();
if (cnode instanceof TNode) {
// this inode is a tomb, has no clients and should be cleaned up
// Because we implemented cleanTomb below, this should be rare, but possible
// Consider calling cleanTomb here too
return Action.OK;
}
if (cnode.containsOnly(clientId) && topic.isEmpty() && cnode.allChildren().isEmpty()) {
// last client to leave this node, AND there are no downstream children, remove via TNode tomb
if (inode == this.root) {
return inode.compareAndSet(cnode, inode.mainNode().copy()) ? Action.OK : Action.REPEAT;
}
TNode tnode = new TNode();
return inode.compareAndSet(cnode, tnode) ? cleanTomb(inode, iParent) : Action.REPEAT;
} else if (cnode.contains(clientId) && topic.isEmpty()) {
CNode updatedCnode = cnode.copy();
updatedCnode.removeSubscriptionsFor(clientId);
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT;
} else {
//someone else already removed
return Action.OK;
}
}
}
@Override
public int size() {
SubscriptionCounterVisitor visitor = new SubscriptionCounterVisitor();
dfsVisit(this.root, visitor, 0);
return visitor.getResult();
}
@Override
public String dumpTree() {
DumpTreeVisitor visitor = new DumpTreeVisitor();
dfsVisit(this.root, visitor, 0);
return visitor.getResult();
}
private void dfsVisit(INode node, IVisitor<?> visitor, int deep) {
if (node == null) {
return;
}
visitor.visit(node.mainNode(), deep);
++deep;
for (INode child : node.mainNode().allChildren()) {
dfsVisit(child, visitor, deep);
}
}
}
@@ -1,67 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.netty.util.internal.StringUtil;
class DumpTreeVisitor implements CTrieSubscriptionDirectory.IVisitor<String> {
String s = "";
@Override
public void visit(CNode node, int deep) {
String indentTabs = indentTabs(deep);
s += indentTabs + (node.token == null ? "''" : node.token.toString()) + prettySubscriptions(node) + "\n";
}
private String prettySubscriptions(CNode node) {
if (node instanceof TNode) {
return "TNode";
}
if (node.subscriptions.isEmpty()) {
return StringUtil.EMPTY_STRING;
}
StringBuilder subScriptionsStr = new StringBuilder(" ~~[");
int counter = 0;
for (Subscription couple : node.subscriptions) {
subScriptionsStr
.append("{filter=").append(couple.topicFilter).append(", ")
.append("client='").append(couple.clientId).append("'}");
counter++;
if (counter < node.subscriptions.size()) {
subScriptionsStr.append(";");
}
}
return subScriptionsStr.append("]").toString();
}
private String indentTabs(int deep) {
StringBuilder s = new StringBuilder();
if (deep > 0) {
s.append(" ");
for (int i = 0; i < deep - 1; i++) {
s.append("| ");
}
s.append("|-");
}
return s.toString();
}
@Override
public String getResult() {
return s;
}
}
@@ -1,45 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import java.util.concurrent.atomic.AtomicReference;
class INode {
private AtomicReference<CNode> mainNode = new AtomicReference<>();
INode(CNode mainNode) {
this.mainNode.set(mainNode);
if (mainNode instanceof TNode) { // this should never happen
throw new IllegalStateException("TNode should not be set on mainNnode");
}
}
boolean compareAndSet(CNode old, CNode newNode) {
return mainNode.compareAndSet(old, newNode);
}
boolean compareAndSet(CNode old, TNode newNode) {
return mainNode.compareAndSet(old, newNode);
}
CNode mainNode() {
return this.mainNode.get();
}
boolean isTombed() {
return this.mainNode() instanceof TNode;
}
}
@@ -1,40 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.broker.ISubscriptionsRepository;
import java.util.List;
import java.util.Set;
public interface ISubscriptionsDirectory {
void init(ISubscriptionsRepository sessionsRepository);
Set<Subscription> matchWithoutQosSharpening(Topic topic);
Set<Subscription> matchQosSharpening(Topic topic);
void add(Subscription newSubscription);
void removeSubscription(Topic topic, String clientID);
List<Subscription> matches(Topic topic);
int size();
String dumpTree();
}
@@ -1,120 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.netty.handler.codec.mqtt.MqttQoS;
import java.io.Serializable;
/**
* Maintain the information about which Topic a certain ClientID is subscribed and at which QoS
*/
public final class Subscription implements Serializable {
private static final long serialVersionUID = -3383457629635732794L;
private final MqttQoS requestedQos; // max QoS acceptable
final String clientId;
final Topic topicFilter;
private final boolean active;
public Subscription(String clientId, Topic topicFilter, MqttQoS requestedQos) {
this.requestedQos = requestedQos;
this.clientId = clientId;
this.topicFilter = topicFilter;
this.active = true;
}
public Subscription(Subscription orig) {
this.requestedQos = orig.requestedQos;
this.clientId = orig.clientId;
this.topicFilter = orig.topicFilter;
this.active = orig.active;
}
/**
* Constructor with undefined maximum QoS
*
* @param clientId id of client owning this subscription.
* @param topicFilter the topic to subscribe to.
* */
public Subscription(String clientId, Topic topicFilter) {
this.requestedQos = null;
this.clientId = clientId;
this.topicFilter = topicFilter;
this.active = true;
}
public String getClientId() {
return clientId;
}
public MqttQoS getRequestedQos() {
return requestedQos;
}
public Topic getTopicFilter() {
return topicFilter;
}
public boolean isActive() {
return active;
}
public boolean qosLessThan(Subscription sub) {
return requestedQos.value() < sub.requestedQos.value();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Subscription that = (Subscription) o;
if (clientId != null ? !clientId.equals(that.clientId) : that.clientId != null)
return false;
return !(topicFilter != null ? !topicFilter.equals(that.topicFilter) : that.topicFilter != null);
}
@Override
public int hashCode() {
int result = clientId != null ? clientId.hashCode() : 0;
result = 31 * result + (topicFilter != null ? topicFilter.hashCode() : 0);
return result;
}
@Override
public String toString() {
return String.format(
"[filter:%s, cliID: %s, qos: %s, active: %s]",
this.topicFilter,
this.clientId,
this.requestedQos,
this.active);
}
@Override
public Subscription clone() {
try {
return (Subscription) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
@@ -1,33 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import java.util.concurrent.atomic.AtomicInteger;
class SubscriptionCounterVisitor implements CTrieSubscriptionDirectory.IVisitor<Integer> {
private AtomicInteger accumulator = new AtomicInteger(0);
@Override
public void visit(CNode node, int deep) {
accumulator.addAndGet(node.subscriptions.size());
}
@Override
public Integer getResult() {
return accumulator.get();
}
}
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
class TNode extends CNode {
@Override
INode childOf(Token token) {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
CNode copy() {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
public void add(INode newINode) {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
CNode addSubscription(Subscription newSubscription) {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
boolean containsOnly(String clientId) {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
public boolean contains(String clientId) {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
void removeSubscriptionsFor(String clientId) {
throw new IllegalStateException("Can't be invoked on TNode");
}
@Override
boolean anyChildrenMatch(Token token) {
return false;
}
}
@@ -1,75 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
/**
* Internal use only class.
*/
public class Token {
static final Token EMPTY = new Token("");
static final Token MULTI = new Token("#");
static final Token SINGLE = new Token("+");
final String name;
protected Token(String s) {
name = s;
}
protected String name() {
return name;
}
protected boolean match(Token t) {
if (MULTI.equals(t) || SINGLE.equals(t)) {
return false;
}
if (MULTI.equals(this) || SINGLE.equals(this)) {
return true;
}
return equals(t);
}
@Override
public int hashCode() {
int hash = 7;
hash = 29 * hash + (this.name != null ? this.name.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Token other = (Token) obj;
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
return false;
}
return true;
}
@Override
public String toString() {
return name;
}
}
@@ -1,218 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Topic implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(Topic.class);
private static final long serialVersionUID = 2438799283749822L;
private final String topic;
private transient List<Token> tokens;
private transient boolean valid;
/**
* Factory method
*
* @param s the topic string (es "/a/b").
* @return the created Topic instance.
* */
public static Topic asTopic(String s) {
return new Topic(s);
}
public Topic(String topic) {
this.topic = topic;
}
Topic(List<Token> tokens) {
this.tokens = tokens;
List<String> strTokens = tokens.stream().map(Token::toString).collect(Collectors.toList());
this.topic = String.join("/", strTokens);
this.valid = true;
}
public List<Token> getTokens() {
if (tokens == null) {
try {
tokens = parseTopic(topic);
valid = true;
} catch (ParseException e) {
valid = false;
LOG.error("Error parsing the topic: {}, message: {}", topic, e.getMessage());
}
}
return tokens;
}
private List<Token> parseTopic(String topic) throws ParseException {
if (topic.length() == 0) {
throw new ParseException("Bad format of topic, topic MUST be at least 1 character [MQTT-4.7.3-1] and " +
"this was empty", 0);
}
List<Token> res = new ArrayList<>();
String[] splitted = topic.split("/");
if (splitted.length == 0) {
res.add(Token.EMPTY);
}
if (topic.endsWith("/")) {
// Add a fictious space
String[] newSplitted = new String[splitted.length + 1];
System.arraycopy(splitted, 0, newSplitted, 0, splitted.length);
newSplitted[splitted.length] = "";
splitted = newSplitted;
}
for (int i = 0; i < splitted.length; i++) {
String s = splitted[i];
if (s.isEmpty()) {
// if (i != 0) {
// throw new ParseException("Bad format of topic, expetec topic name between
// separators", i);
// }
res.add(Token.EMPTY);
} else if (s.equals("#")) {
// check that multi is the last symbol
if (i != splitted.length - 1) {
throw new ParseException(
"Bad format of topic, the multi symbol (#) has to be the last one after a separator",
i);
}
res.add(Token.MULTI);
} else if (s.contains("#")) {
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
} else if (s.equals("+")) {
res.add(Token.SINGLE);
} else if (s.contains("+")) {
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
} else {
res.add(new Token(s));
}
}
return res;
}
public Token headToken() {
final List<Token> tokens = getTokens();
if (tokens.isEmpty()) {
//TODO UGLY use Optional
return null;
}
return tokens.get(0);
}
public boolean isEmpty() {
final List<Token> tokens = getTokens();
return tokens == null || tokens.isEmpty();
}
/**
* @return a new Topic corresponding to this less than the head token
* */
public Topic exceptHeadToken() {
List<Token> tokens = getTokens();
if (tokens.isEmpty()) {
return new Topic(Collections.emptyList());
}
List<Token> tokensCopy = new ArrayList<>(tokens);
tokensCopy.remove(0);
return new Topic(tokensCopy);
}
public boolean isValid() {
if (tokens == null)
getTokens();
return valid;
}
/**
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
*
* @param subscriptionTopic
* the topic filter of the subscription
* @return true if the two topics match.
*/
// TODO reimplement with iterators or with queues
public boolean match(Topic subscriptionTopic) {
List<Token> msgTokens = getTokens();
List<Token> subscriptionTokens = subscriptionTopic.getTokens();
int i = 0;
for (; i < subscriptionTokens.size(); i++) {
Token subToken = subscriptionTokens.get(i);
if (!Token.MULTI.equals(subToken) && !Token.SINGLE.equals(subToken)) {
if (i >= msgTokens.size()) {
return false;
}
Token msgToken = msgTokens.get(i);
if (!msgToken.equals(subToken)) {
return false;
}
} else {
if (Token.MULTI.equals(subToken)) {
return true;
}
// if (Token.SINGLE.equals(subToken)) {
// // skip a step forward
// }
}
}
return i == msgTokens.size();
}
@Override
public String toString() {
return topic;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Topic other = (Topic) obj;
return Objects.equals(this.topic, other.topic);
}
@Override
public int hashCode() {
return topic.hashCode();
}
}