mirror of
https://github.com/eclipse/paho.mqtt.java.git
synced 2025-10-16 14:17:07 +08:00
added a testcase and fixed Issue-918
This commit is contained in:
@@ -38,7 +38,7 @@ public class MqttTopicTest {
|
|||||||
String[][] matchingTopics = new String[][] { { "sport/tennis/player1/#", "sport/tennis/player1" },
|
String[][] matchingTopics = new String[][] { { "sport/tennis/player1/#", "sport/tennis/player1" },
|
||||||
{ "sport/tennis/player1/#", "sport/tennis/player1/ranking" },
|
{ "sport/tennis/player1/#", "sport/tennis/player1/ranking" },
|
||||||
{ "sport/tennis/player1/#", "sport/tennis/player1/score/wimbledon" }, { "sport/#", "sport" },
|
{ "sport/tennis/player1/#", "sport/tennis/player1/score/wimbledon" }, { "sport/#", "sport" },
|
||||||
{ "#", "sport/tennis/player1" } };
|
{ "#", "sport/tennis/player1" } , {"sport/+/player1/ranking/#","sport/tennis/player1/ranking"} };
|
||||||
|
|
||||||
for (String[] pair : matchingTopics) {
|
for (String[] pair : matchingTopics) {
|
||||||
Assert.assertTrue(pair[0] + " should match " + pair[1], MqttTopicValidator.isMatched(pair[0], pair[1]));
|
Assert.assertTrue(pair[0] + " should match " + pair[1], MqttTopicValidator.isMatched(pair[0], pair[1]));
|
||||||
|
@@ -4,214 +4,222 @@ import java.io.UnsupportedEncodingException;
|
|||||||
|
|
||||||
public class MqttTopicValidator {
|
public class MqttTopicValidator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The forward slash (/) is used to separate each level within a topic tree and
|
* The forward slash (/) is used to separate each level within a topic tree and provide a hierarchical structure to
|
||||||
* provide a hierarchical structure to the topic space. The use of the topic
|
* the topic space. The use of the topic level separator is significant when the two wildcard characters are
|
||||||
* level separator is significant when the two wildcard characters are
|
* encountered in topics specified by subscribers.
|
||||||
* encountered in topics specified by subscribers.
|
*/
|
||||||
*/
|
public static final String TOPIC_LEVEL_SEPARATOR = "/";
|
||||||
public static final String TOPIC_LEVEL_SEPARATOR = "/";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multi-level wildcard The number sign (#) is a wildcard character that matches
|
* Multi-level wildcard The number sign (#) is a wildcard character that matches any number of levels within a topic.
|
||||||
* any number of levels within a topic.
|
*/
|
||||||
*/
|
public static final String MULTI_LEVEL_WILDCARD = "#";
|
||||||
public static final String MULTI_LEVEL_WILDCARD = "#";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single-level wildcard The plus sign (+) is a wildcard character that matches
|
* Single-level wildcard The plus sign (+) is a wildcard character that matches only one topic level.
|
||||||
* only one topic level.
|
*/
|
||||||
*/
|
public static final String SINGLE_LEVEL_WILDCARD = "+";
|
||||||
public static final String SINGLE_LEVEL_WILDCARD = "+";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multi-level wildcard pattern(/#)
|
* Multi-level wildcard pattern(/#)
|
||||||
*/
|
*/
|
||||||
public static final String MULTI_LEVEL_WILDCARD_PATTERN = TOPIC_LEVEL_SEPARATOR + MULTI_LEVEL_WILDCARD;
|
public static final String MULTI_LEVEL_WILDCARD_PATTERN = TOPIC_LEVEL_SEPARATOR + MULTI_LEVEL_WILDCARD;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Topic wildcards (#+)
|
* Topic wildcards (#+)
|
||||||
*/
|
*/
|
||||||
public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD;
|
public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD;
|
||||||
|
|
||||||
// topic name and topic filter length range defined in the spec
|
// topic name and topic filter length range defined in the spec
|
||||||
private static final int MIN_TOPIC_LEN = 1;
|
private static final int MIN_TOPIC_LEN = 1;
|
||||||
private static final int MAX_TOPIC_LEN = 65535;
|
private static final int MAX_TOPIC_LEN = 65535;
|
||||||
private static final char NUL = '\u0000';
|
private static final char NUL = '\u0000';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the topic name or topic filter
|
* Validate the topic name or topic filter
|
||||||
*
|
*
|
||||||
* @param topicString
|
* @param topicString
|
||||||
* topic name or filter
|
* topic name or filter
|
||||||
* @param wildcardAllowed
|
* @param wildcardAllowed
|
||||||
* true if validate topic filter, false otherwise
|
* true if validate topic filter, false otherwise
|
||||||
* @param sharedSubAllowed
|
* @param sharedSubAllowed
|
||||||
* true if shared subscription is allowed, false otherwise
|
* true if shared subscription is allowed, false otherwise
|
||||||
* @throws IllegalArgumentException
|
* @throws IllegalArgumentException
|
||||||
* if the topic is invalid
|
* if the topic is invalid
|
||||||
*/
|
*/
|
||||||
public static void validate(String topicString, boolean wildcardAllowed, boolean sharedSubAllowed)
|
public static void validate(String topicString, boolean wildcardAllowed, boolean sharedSubAllowed)
|
||||||
throws IllegalArgumentException {
|
throws IllegalArgumentException {
|
||||||
int topicLen = 0;
|
int topicLen = 0;
|
||||||
try {
|
try {
|
||||||
topicLen = topicString.getBytes("UTF-8").length;
|
topicLen = topicString.getBytes("UTF-8").length;
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
throw new IllegalStateException(e.getMessage());
|
throw new IllegalStateException(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spec: length check
|
// Spec: length check
|
||||||
// - All Topic Names and Topic Filters MUST be at least one character
|
// - All Topic Names and Topic Filters MUST be at least one character
|
||||||
// long
|
// long
|
||||||
// - Topic Names and Topic Filters are UTF-8 encoded strings, they MUST
|
// - Topic Names and Topic Filters are UTF-8 encoded strings, they MUST
|
||||||
// NOT encode to more than 65535 bytes
|
// NOT encode to more than 65535 bytes
|
||||||
if (topicLen < MIN_TOPIC_LEN || topicLen > MAX_TOPIC_LEN) {
|
if (topicLen < MIN_TOPIC_LEN || topicLen > MAX_TOPIC_LEN) {
|
||||||
throw new IllegalArgumentException(String.format("Invalid topic length, should be in range[%d, %d]!",
|
throw new IllegalArgumentException(String.format("Invalid topic length, should be in range[%d, %d]!",
|
||||||
new Object[] { Integer.valueOf(MIN_TOPIC_LEN), Integer.valueOf(MAX_TOPIC_LEN) }));
|
new Object[] { Integer.valueOf(MIN_TOPIC_LEN), Integer.valueOf(MAX_TOPIC_LEN) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// *******************************************************************************
|
// *******************************************************************************
|
||||||
// 1) This is a topic filter string that can contain wildcard characters
|
// 1) This is a topic filter string that can contain wildcard characters
|
||||||
// *******************************************************************************
|
// *******************************************************************************
|
||||||
if (wildcardAllowed) {
|
if (wildcardAllowed) {
|
||||||
// Only # or +
|
// Only # or +
|
||||||
if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) {
|
if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Check multi-level wildcard
|
// 1) Check multi-level wildcard
|
||||||
// Rule:
|
// Rule:
|
||||||
// The multi-level wildcard can be specified only on its own or next
|
// The multi-level wildcard can be specified only on its own or next
|
||||||
// to the topic level separator character.
|
// to the topic level separator character.
|
||||||
|
|
||||||
// - Can only contains one multi-level wildcard character
|
// - Can only contains one multi-level wildcard character
|
||||||
// - The multi-level wildcard must be the last character used within
|
// - The multi-level wildcard must be the last character used within
|
||||||
// the topic tree
|
// the topic tree
|
||||||
if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1
|
if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1
|
||||||
|| (topicString.contains(MULTI_LEVEL_WILDCARD)
|
|| (topicString.contains(MULTI_LEVEL_WILDCARD) && !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) {
|
||||||
&& !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) {
|
throw new IllegalArgumentException("Invalid usage of multi-level wildcard in topic string: " + topicString);
|
||||||
throw new IllegalArgumentException(
|
}
|
||||||
"Invalid usage of multi-level wildcard in topic string: " + topicString);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Check single-level wildcard
|
// 2) Check single-level wildcard
|
||||||
// Rule:
|
// Rule:
|
||||||
// The single-level wildcard can be used at any level in the topic
|
// The single-level wildcard can be used at any level in the topic
|
||||||
// tree, and in conjunction with the
|
// tree, and in conjunction with the
|
||||||
// multilevel wildcard. It must be used next to the topic level
|
// multilevel wildcard. It must be used next to the topic level
|
||||||
// separator, except when it is specified on
|
// separator, except when it is specified on
|
||||||
// its own.
|
// its own.
|
||||||
validateSingleLevelWildcard(topicString);
|
validateSingleLevelWildcard(topicString);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Shared Subscriptions
|
// Validate Shared Subscriptions
|
||||||
if (!sharedSubAllowed && topicString.startsWith("$share/")) {
|
if (!sharedSubAllowed && topicString.startsWith("$share/")) {
|
||||||
throw new IllegalArgumentException("Shared Subscriptions are not allowed.");
|
throw new IllegalArgumentException("Shared Subscriptions are not allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// *******************************************************************************
|
// *******************************************************************************
|
||||||
// 2) This is a topic name string that MUST NOT contains any wildcard characters
|
// 2) This is a topic name string that MUST NOT contains any wildcard characters
|
||||||
// *******************************************************************************
|
// *******************************************************************************
|
||||||
if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) {
|
if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) {
|
||||||
throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)");
|
throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateSingleLevelWildcard(String topicString) {
|
private static void validateSingleLevelWildcard(String topicString) {
|
||||||
char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0);
|
char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0);
|
||||||
char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0);
|
char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0);
|
||||||
|
|
||||||
char[] chars = topicString.toCharArray();
|
char[] chars = topicString.toCharArray();
|
||||||
int length = chars.length;
|
int length = chars.length;
|
||||||
char prev = NUL, next = NUL;
|
char prev = NUL, next = NUL;
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
prev = (i - 1 >= 0) ? chars[i - 1] : NUL;
|
prev = (i - 1 >= 0) ? chars[i - 1] : NUL;
|
||||||
next = (i + 1 < length) ? chars[i + 1] : NUL;
|
next = (i + 1 < length) ? chars[i + 1] : NUL;
|
||||||
|
|
||||||
if (chars[i] == singleLevelWildcardChar) {
|
if (chars[i] == singleLevelWildcardChar) {
|
||||||
// prev and next can be only '/' or none
|
// prev and next can be only '/' or none
|
||||||
if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) {
|
if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(String
|
||||||
String.format("Invalid usage of single-level wildcard in topic string '%s'!",
|
.format("Invalid usage of single-level wildcard in topic string '%s'!", new Object[] { topicString }));
|
||||||
new Object[] { topicString }));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the supplied topic name and filter match
|
* Check the supplied topic name and filter match
|
||||||
*
|
*
|
||||||
* @param topicFilter
|
* @param topicFilter
|
||||||
* topic filter: wildcards allowed
|
* topic filter: wildcards allowed
|
||||||
* @param topicName
|
* @param topicName
|
||||||
* topic name: wildcards not allowed
|
* topic name: wildcards not allowed
|
||||||
* @return true if the topic matches the filter
|
* @return true if the topic matches the filter
|
||||||
* @throws IllegalArgumentException
|
* @throws IllegalArgumentException
|
||||||
* if the topic name or filter is invalid
|
* if the topic name or filter is invalid
|
||||||
*/
|
*/
|
||||||
public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException {
|
public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException {
|
||||||
int topicPos = 0;
|
int topicPos = 0;
|
||||||
int filterPos = 0;
|
int filterPos = 0;
|
||||||
int topicLen = topicName.length();
|
int topicLen = topicName.length();
|
||||||
int filterLen = topicFilter.length();
|
int filterLen = topicFilter.length();
|
||||||
|
|
||||||
MqttTopicValidator.validate(topicFilter, true, true);
|
MqttTopicValidator.validate(topicFilter, true, true);
|
||||||
MqttTopicValidator.validate(topicName, false, true);
|
MqttTopicValidator.validate(topicName, false, true);
|
||||||
|
|
||||||
if (topicFilter.equals(topicName)) {
|
if (topicFilter.equals(topicName)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (filterPos < filterLen && topicPos < topicLen) {
|
while (filterPos < filterLen && topicPos < topicLen) {
|
||||||
if (topicFilter.charAt(filterPos) == '#') {
|
if (topicFilter.charAt(filterPos) == '#') {
|
||||||
/*
|
/*
|
||||||
* next 'if' will break when topicFilter = topic/# and topicName topic/A/,
|
* next 'if' will break when topicFilter = topic/# and topicName topic/A/, but they are matched
|
||||||
* but they are matched
|
*/
|
||||||
*/
|
topicPos = topicLen;
|
||||||
topicPos = topicLen;
|
filterPos = filterLen;
|
||||||
filterPos = filterLen;
|
break;
|
||||||
break;
|
}
|
||||||
}
|
if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/')
|
||||||
if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/')
|
break;
|
||||||
break;
|
if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#'
|
||||||
if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#'
|
&& topicFilter.charAt(filterPos) != topicName.charAt(topicPos))
|
||||||
&& topicFilter.charAt(filterPos) != topicName.charAt(topicPos))
|
break;
|
||||||
break;
|
if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string
|
||||||
if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string
|
int nextpos = topicPos + 1;
|
||||||
int nextpos = topicPos + 1;
|
while (nextpos < topicLen && topicName.charAt(nextpos) != '/')
|
||||||
while (nextpos < topicLen && topicName.charAt(nextpos) != '/')
|
nextpos = ++topicPos + 1;
|
||||||
nextpos = ++topicPos + 1;
|
} else if (topicFilter.charAt(filterPos) == '#')
|
||||||
} else if (topicFilter.charAt(filterPos) == '#')
|
topicPos = topicLen - 1; // skip until end of string
|
||||||
topicPos = topicLen - 1; // skip until end of string
|
filterPos++;
|
||||||
filterPos++;
|
topicPos++;
|
||||||
topicPos++;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ((topicPos == topicLen) && (filterPos == filterLen)) {
|
if ((topicPos == topicLen) && (filterPos == filterLen)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to
|
* https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to match sport/# to sport
|
||||||
* match sport/# to sport
|
*/
|
||||||
*/
|
if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) {
|
||||||
if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) {
|
if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#')
|
||||||
if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#')
|
return true;
|
||||||
return true;
|
if (topicFilter.length() - filterPos > 1 && topicFilter.substring(filterPos, filterPos + 2).equals("/#")) {
|
||||||
if (topicFilter.length() - filterPos > 1
|
if ((topicFilter.length() - topicName.length()) == 2
|
||||||
&& topicFilter.substring(filterPos, filterPos + 2).equals("/#")) {
|
&& topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) {
|
||||||
if ((topicFilter.length() - topicName.length()) == 2
|
return true;
|
||||||
&& topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) {
|
}
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
}
|
/*
|
||||||
}
|
* https://github.com/eclipse/paho.mqtt.java/issues/918
|
||||||
}
|
* covers cases that include more then one wildcard
|
||||||
return false;
|
* sport/+/tennis/#
|
||||||
}
|
*/
|
||||||
|
String[] topicFilterParts = topicFilter.split(TOPIC_LEVEL_SEPARATOR);
|
||||||
|
String[] topicParts = topicName.split(TOPIC_LEVEL_SEPARATOR);
|
||||||
|
if(topicFilterParts.length -1 == topicParts.length &&
|
||||||
|
topicFilterParts[topicFilterParts.length-1].equals( MULTI_LEVEL_WILDCARD)) {
|
||||||
|
for (int i = 0; i<topicParts.length;i++) {
|
||||||
|
if(!topicParts[i].equals(topicFilterParts[i]) && !topicFilterParts[i].equals(SINGLE_LEVEL_WILDCARD)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user