Moved project package from org.eclipse.moquette to io.moquette

This commit is contained in:
andsel
2015-11-20 21:30:59 +01:00
parent f438ef7f49
commit 371c740321
175 changed files with 629 additions and 669 deletions
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2012-2015 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 io.moquette.proto.messages.AbstractMessage.QOSType;
/**
* Maintain the information about which Topic a certain ClientID is subscribed
* and at which QoS
*
*
* @author andrea
*/
public class Subscription implements Serializable {
QOSType requestedQos; //max QoS acceptable
String clientId;
String topicFilter;
boolean cleanSession;
boolean active = true;
public Subscription(String clientId, String topicFilter, QOSType requestedQos, boolean cleanSession) {
this.requestedQos = requestedQos;
this.clientId = clientId;
this.topicFilter = topicFilter;
this.cleanSession = cleanSession;
}
public Subscription(Subscription orig) {
this.requestedQos = orig.requestedQos;
this.clientId = orig.clientId;
this.topicFilter = orig.topicFilter;
this.cleanSession = orig.cleanSession;
this.active = orig.active;
}
public String getClientId() {
return clientId;
}
public QOSType getRequestedQos() {
return requestedQos;
}
public String getTopicFilter() {
return topicFilter;
}
public boolean isCleanSession() {
return this.cleanSession;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Subscription that = (Subscription) o;
if (cleanSession != that.cleanSession) return false;
if (clientId != null ? !clientId.equals(that.clientId) : that.clientId != null) return false;
if (requestedQos != that.requestedQos) return false;
if (topicFilter != null ? !topicFilter.equals(that.topicFilter) : that.topicFilter != null) return false;
return true;
}
@Override
public int hashCode() {
int result = requestedQos.hashCode();
result = 31 * result + (clientId != null ? clientId.hashCode() : 0);
result = 31 * result + (topicFilter != null ? topicFilter.hashCode() : 0);
result = 31 * result + (cleanSession ? 1 : 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;
}
}
}
@@ -0,0 +1,339 @@
/*
* Copyright (c) 2012-2015 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.text.ParseException;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
import io.moquette.spi.ISessionsStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a tree of topics subscriptions.
*
* @author andrea
*/
public class SubscriptionsStore {
public static class NodeCouple {
final TreeNode root;
final TreeNode createdNode;
public NodeCouple(TreeNode root, TreeNode createdNode) {
this.root = root;
this.createdNode = createdNode;
}
}
/**
* Check if the topic filter of the subscription is well formed
* */
public static boolean validate(String topicFilter) {
try {
parseTopic(topicFilter);
return true;
} catch (ParseException pex) {
LOG.info("Bad matching topic filter <{}>", topicFilter);
return false;
}
}
public interface IVisitor<T> {
void visit(TreeNode node, int deep);
T getResult();
}
private class DumpTreeVisitor implements IVisitor<String> {
String s = "";
@Override
public void visit(TreeNode node, int deep) {
String subScriptionsStr = "";
String indentTabs = indentTabs(deep);
for (Subscription sub : node.m_subscriptions) {
subScriptionsStr += indentTabs + sub.toString() + "\n";
}
s += node.getToken() == null ? "" : node.getToken().toString();
s += "\n" + (node.m_subscriptions.isEmpty() ? indentTabs : "") + subScriptionsStr /*+ "\n"*/;
}
private String indentTabs(int deep) {
String s = "";
for (int i=0; i < deep; i++) {
s += "\t";
// s += "--";
}
return s;
}
@Override
public String getResult() {
return s;
}
}
private AtomicReference<TreeNode> subscriptions = new AtomicReference<>(new TreeNode(null));
private static final Logger LOG = LoggerFactory.getLogger(SubscriptionsStore.class);
/**
* Initialize the subscription tree with the list of subscriptions.
* Maintained for compatibility reasons.
*/
public void init(ISessionsStore sessionsStore) {
LOG.debug("init invoked");
List<Subscription> subscriptions = sessionsStore.listAllSubscriptions();
//reload any subscriptions persisted
if (LOG.isDebugEnabled()) {
LOG.debug("Reloading all stored subscriptions...subscription tree before {}", dumpTree());
}
for (Subscription subscription : subscriptions) {
LOG.debug("Re-subscribing {} to topic {}", subscription.getClientId(), subscription.getTopicFilter());
add(subscription);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Finished loading. Subscription tree after {}", dumpTree());
}
}
public void add(Subscription newSubscription) {
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(newSubscription.topicFilter, oldRoot);
couple.createdNode.addSubscription(newSubscription); //createdNode could be null?
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while(!subscriptions.compareAndSet(oldRoot, couple.root));
LOG.debug("root ref {}, original root was {}", couple.root, oldRoot);
}
protected NodeCouple recreatePath(String topic, final TreeNode oldRoot) {
List<Token> tokens = new ArrayList<>();
try {
tokens = parseTopic(topic);
} catch (ParseException ex) {
//TODO handle the parse exception
LOG.error(null, ex);
}
final TreeNode newRoot = oldRoot.copy();
TreeNode parent = newRoot;
TreeNode current = newRoot;
for (Token token : tokens) {
TreeNode matchingChildren;
//check if a children with the same token already exists
if ((matchingChildren = current.childWithToken(token)) != null) {
//copy the traversed node
current = matchingChildren.copy();
current.m_parent = parent;
//update the child just added in the children list
parent.updateChild(matchingChildren, current);
parent = current;
} else {
//create a new node for the newly inserted token
matchingChildren = new TreeNode(current);
matchingChildren.setToken(token);
current.addChild(matchingChildren);
current = matchingChildren;
}
}
return new NodeCouple(newRoot, current);
}
public void removeSubscription(String topic, String clientID) {
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(topic, oldRoot);
//do the job
//search for the subscription to remove
Subscription toBeRemoved = null;
for (Subscription sub : couple.createdNode.subscriptions()) {
if (sub.topicFilter.equals(topic) && sub.getClientId().equals(clientID)) {
toBeRemoved = sub;
break;
}
}
if (toBeRemoved != null) {
couple.createdNode.subscriptions().remove(toBeRemoved);
}
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while(!subscriptions.compareAndSet(oldRoot, couple.root));
}
/**
* Visit the topics tree to remove matching subscriptions with clientID.
* It's a mutating structure operation so create a new subscription tree (partial or total).
*/
public void removeForClient(String clientID) {
TreeNode oldRoot;
TreeNode newRoot;
do {
oldRoot = subscriptions.get();
newRoot = oldRoot.removeClientSubscriptions(clientID);
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while(!subscriptions.compareAndSet(oldRoot, newRoot));
}
/**
* 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.
*/
public List<Subscription> matches(String topic) {
List<Token> tokens;
try {
tokens = parseTopic(topic);
} catch (ParseException ex) {
//TODO handle the parse exception
LOG.error(null, ex);
return Collections.emptyList();
}
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(tokens);
List<Subscription> matchingSubs = new ArrayList<>();
subscriptions.get().matches(tokenQueue, matchingSubs);
//remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = new HashMap<>();
for (Subscription sub : matchingSubs) {
Subscription existingSub = subsForClient.get(sub.getClientId());
//update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().byteValue() < sub.getRequestedQos().byteValue()) {
subsForClient.put(sub.getClientId(), sub);
}
}
return new ArrayList<>(subsForClient.values());
}
public boolean contains(Subscription sub) {
return !matches(sub.topicFilter).isEmpty();
}
public int size() {
return subscriptions.get().size();
}
public String dumpTree() {
DumpTreeVisitor visitor = new DumpTreeVisitor();
bfsVisit(subscriptions.get(), visitor, 0);
return visitor.getResult();
}
private void bfsVisit(TreeNode node, IVisitor visitor, int deep) {
if (node == null) {
return;
}
visitor.visit(node, deep);
for (TreeNode child : node.m_children) {
bfsVisit(child, visitor, ++deep);
}
}
/**
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
*/
//TODO reimplement with iterators or with queues
public static boolean matchTopics(String msgTopic, String subscriptionTopic) {
try {
List<Token> msgTokens = SubscriptionsStore.parseTopic(msgTopic);
List<Token> subscriptionTokens = SubscriptionsStore.parseTopic(subscriptionTopic);
int i = 0;
for (; i< subscriptionTokens.size(); i++) {
Token subToken = subscriptionTokens.get(i);
if (subToken != Token.MULTI && subToken != Token.SINGLE) {
if (i >= msgTokens.size()) {
return false;
}
Token msgToken = msgTokens.get(i);
if (!msgToken.equals(subToken)) {
return false;
}
} else {
if (subToken == Token.MULTI) {
return true;
}
if (subToken == Token.SINGLE) {
//skip a step forward
}
}
}
//if last token was a SINGLE then treat it as an empty
// if (subToken == Token.SINGLE && (i - msgTokens.size() == 1)) {
// i--;
// }
return i == msgTokens.size();
} catch (ParseException ex) {
LOG.error(null, ex);
throw new RuntimeException(ex);
}
}
protected static List<Token> parseTopic(String topic) throws ParseException {
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;
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2012-2015 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 (t == MULTI || t == SINGLE) {
return false;
}
if (this == MULTI || this == SINGLE) {
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;
}
}
@@ -0,0 +1,192 @@
/*
* Copyright (c) 2012-2015 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.*;
import java.util.concurrent.LinkedBlockingQueue;
class TreeNode {
private class ClientIDComparator implements Comparator<Subscription> {
public int compare(Subscription o1, Subscription o2) {
return o1.getClientId().compareTo(o2.getClientId());
}
}
TreeNode m_parent;
Token m_token;
List<TreeNode> m_children = new ArrayList<>();
List<Subscription> m_subscriptions = new ArrayList<>();
TreeNode(TreeNode parent) {
this.m_parent = parent;
}
Token getToken() {
return m_token;
}
void setToken(Token topic) {
this.m_token = topic;
}
void addSubscription(Subscription s) {
//avoid double registering for same clientID, topic and QoS
if (m_subscriptions.contains(s)) {
return;
}
//remove existing subscription for same client and topic but different QoS
Comparator<Subscription> comparator = new ClientIDComparator();
Collections.sort(m_subscriptions, comparator);
int existingSubIdx = Collections.binarySearch(m_subscriptions, s, comparator);
if (existingSubIdx >= 0) {
m_subscriptions.remove(existingSubIdx);
}
m_subscriptions.add(s);
}
void addChild(TreeNode child) {
m_children.add(child);
}
/**
* Creates a shallow copy of the current node.
* Copy the token and the children.
* */
TreeNode copy() {
final TreeNode copy = new TreeNode(this);
copy.m_parent = m_parent;
copy.m_children = new ArrayList<>(m_children);
copy.m_subscriptions = new ArrayList<>(m_subscriptions.size());
for (Subscription sub : m_subscriptions) {
copy.m_subscriptions.add(new Subscription(sub));
}
copy.m_token = m_token;
return copy;
}
/**
* Search for children that has the specified token, if not found return
* null;
*/
TreeNode childWithToken(Token token) {
for (TreeNode child : m_children) {
if (child.getToken().equals(token)) {
return child;
}
}
return null;
}
void updateChild(TreeNode oldChild, TreeNode newChild) {
m_children.remove(oldChild);
m_children.add(newChild);
}
List<Subscription> subscriptions() {
return m_subscriptions;
}
void matches(Queue<Token> tokens, List<Subscription> matchingSubs) {
Token t = tokens.poll();
//check if t is null <=> tokens finished
if (t == null) {
matchingSubs.addAll(m_subscriptions);
//check if it has got a MULTI child and add its subscriptions
for (TreeNode n : m_children) {
if (n.getToken() == Token.MULTI || n.getToken() == Token.SINGLE) {
matchingSubs.addAll(n.subscriptions());
}
}
return;
}
//we are on MULTI, than add subscriptions and return
if (m_token == Token.MULTI) {
matchingSubs.addAll(m_subscriptions);
return;
}
for (TreeNode n : m_children) {
if (n.getToken().match(t)) {
//Create a copy of token, else if navigate 2 sibling it
//consumes 2 elements on the queue instead of one
n.matches(new LinkedBlockingQueue<>(tokens), matchingSubs);
//TODO don't create a copy n.matches(tokens, matchingSubs);
}
}
}
/**
* Return the number of registered subscriptions
*/
int size() {
int res = m_subscriptions.size();
for (TreeNode child : m_children) {
res += child.size();
}
return res;
}
/**
* Create a copied subtree rooted on this node but purged of clientID's subscriptions.
* */
TreeNode removeClientSubscriptions(String clientID) {
//collect what to delete and then delete to avoid ConcurrentModification
TreeNode newSubRoot = this.copy();
List<Subscription> subsToRemove = new ArrayList<>();
for (Subscription s : newSubRoot.m_subscriptions) {
if (s.clientId.equals(clientID)) {
subsToRemove.add(s);
}
}
for (Subscription s : subsToRemove) {
newSubRoot.m_subscriptions.remove(s);
}
//go deep
List<TreeNode> newChildren = new ArrayList<>(newSubRoot.m_children.size());
for (TreeNode child : newSubRoot.m_children) {
newChildren.add(child.removeClientSubscriptions(clientID));
}
newSubRoot.m_children = newChildren;
return newSubRoot;
}
/**
* @return the set of subscriptions for the given client.
* */
Set<Subscription> findAllByClientID(String clientID) {
Set<Subscription> subs = new HashSet<>();
for (Subscription s : m_subscriptions) {
if (s.clientId.equals(clientID)) {
subs.add(s);
}
}
//go deep
for (TreeNode child : m_children) {
subs.addAll(child.findAllByClientID(clientID));
}
return subs;
}
}