PageRenderTime 52ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/org.eclipse.paho.client.mqttv3/src/main/java/org/eclipse/paho/client/mqttv3/MqttTopic.java

http://github.com/eclipse/paho.mqtt.java
Java | 321 lines | 133 code | 35 blank | 153 comment | 53 complexity | 0f3a5401f6e73ecfee1fcff6ef4d7453 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. /*******************************************************************************
  2. * Copyright (c) 2009, 2014 IBM Corp.
  3. *
  4. * All rights reserved. This program and the accompanying materials
  5. * are made available under the terms of the Eclipse Public License v2.0
  6. * and Eclipse Distribution License v1.0 which accompany this distribution.
  7. *
  8. * The Eclipse Public License is available at
  9. * https://www.eclipse.org/legal/epl-2.0
  10. * and the Eclipse Distribution License is available at
  11. * https://www.eclipse.org/org/documents/edl-v10.php
  12. *
  13. * Contributors:
  14. * Dave Locke - initial API and implementation and/or initial documentation
  15. */
  16. package org.eclipse.paho.client.mqttv3;
  17. import java.io.UnsupportedEncodingException;
  18. import org.eclipse.paho.client.mqttv3.internal.ClientComms;
  19. import org.eclipse.paho.client.mqttv3.internal.wire.MqttPublish;
  20. import org.eclipse.paho.client.mqttv3.util.Strings;
  21. /**
  22. * Represents a topic destination, used for publish/subscribe messaging.
  23. */
  24. public class MqttTopic {
  25. /**
  26. * The forward slash (/) is used to separate each level within a topic tree and
  27. * provide a hierarchical structure to the topic space. The use of the topic
  28. * level separator is significant when the two wildcard characters are
  29. * encountered in topics specified by subscribers.
  30. */
  31. public static final String TOPIC_LEVEL_SEPARATOR = "/";
  32. /**
  33. * Multi-level wildcard The number sign (#) is a wildcard character that matches
  34. * any number of levels within a topic.
  35. */
  36. public static final String MULTI_LEVEL_WILDCARD = "#";
  37. /**
  38. * Single-level wildcard The plus sign (+) is a wildcard character that matches
  39. * only one topic level.
  40. */
  41. public static final String SINGLE_LEVEL_WILDCARD = "+";
  42. /**
  43. * Multi-level wildcard pattern(/#)
  44. */
  45. public static final String MULTI_LEVEL_WILDCARD_PATTERN = TOPIC_LEVEL_SEPARATOR + MULTI_LEVEL_WILDCARD;
  46. /**
  47. * Topic wildcards (#+)
  48. */
  49. public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD;
  50. // topic name and topic filter length range defined in the spec
  51. private static final int MIN_TOPIC_LEN = 1;
  52. private static final int MAX_TOPIC_LEN = 65535;
  53. private static final char NUL = '\u0000';
  54. private ClientComms comms;
  55. private String name;
  56. /**
  57. * @param name
  58. * The Name of the topic
  59. * @param comms
  60. * The {@link ClientComms}
  61. */
  62. public MqttTopic(String name, ClientComms comms) {
  63. this.comms = comms;
  64. this.name = name;
  65. }
  66. /**
  67. * Publishes a message on the topic. This is a convenience method, which will
  68. * create a new {@link MqttMessage} object with a byte array payload and the
  69. * specified QoS, and then publish it. All other values in the message will be
  70. * set to the defaults.
  71. *
  72. * @param payload
  73. * the byte array to use as the payload
  74. * @param qos
  75. * the Quality of Service. Valid values are 0, 1 or 2.
  76. * @param retained
  77. * whether or not this message should be retained by the server.
  78. * @return {@link MqttDeliveryToken}
  79. * @throws MqttException
  80. * If an error occurs publishing the message
  81. * @throws MqttPersistenceException
  82. * If an error occurs persisting the message
  83. * @throws IllegalArgumentException
  84. * if value of QoS is not 0, 1 or 2.
  85. * @see #publish(MqttMessage)
  86. * @see MqttMessage#setQos(int)
  87. * @see MqttMessage#setRetained(boolean)
  88. */
  89. public MqttDeliveryToken publish(byte[] payload, int qos, boolean retained)
  90. throws MqttException, MqttPersistenceException {
  91. MqttMessage message = new MqttMessage(payload);
  92. message.setQos(qos);
  93. message.setRetained(retained);
  94. return this.publish(message);
  95. }
  96. /**
  97. * Publishes the specified message to this topic, but does not wait for delivery
  98. * of the message to complete. The returned {@link MqttDeliveryToken token} can
  99. * be used to track the delivery status of the message. Once this method has
  100. * returned cleanly, the message has been accepted for publication by the
  101. * client. Message delivery will be completed in the background when a
  102. * connection is available.
  103. *
  104. * @param message
  105. * the message to publish
  106. * @return an MqttDeliveryToken for tracking the delivery of the message
  107. * @throws MqttException
  108. * if an error occurs publishing the message
  109. * @throws MqttPersistenceException
  110. * if an error occurs persisting the message
  111. */
  112. public MqttDeliveryToken publish(MqttMessage message) throws MqttException, MqttPersistenceException {
  113. MqttDeliveryToken token = new MqttDeliveryToken(comms.getClient().getClientId());
  114. token.setMessage(message);
  115. comms.sendNoWait(createPublish(message), token);
  116. token.internalTok.waitUntilSent();
  117. return token;
  118. }
  119. /**
  120. * Returns the name of the queue or topic.
  121. *
  122. * @return the name of this destination.
  123. */
  124. public String getName() {
  125. return name;
  126. }
  127. /**
  128. * Create a PUBLISH packet from the specified message.
  129. */
  130. private MqttPublish createPublish(MqttMessage message) {
  131. return new MqttPublish(this.getName(), message);
  132. }
  133. /**
  134. * Returns a string representation of this topic.
  135. *
  136. * @return a string representation of this topic.
  137. */
  138. public String toString() {
  139. return getName();
  140. }
  141. /**
  142. * Validate the topic name or topic filter
  143. *
  144. * @param topicString
  145. * topic name or filter
  146. * @param wildcardAllowed
  147. * true if validate topic filter, false otherwise
  148. * @throws IllegalArgumentException
  149. * if the topic is invalid
  150. */
  151. public static void validate(String topicString, boolean wildcardAllowed) throws IllegalArgumentException {
  152. int topicLen = 0;
  153. try {
  154. topicLen = topicString.getBytes("UTF-8").length;
  155. } catch (UnsupportedEncodingException e) {
  156. throw new IllegalStateException(e.getMessage());
  157. }
  158. // Spec: length check
  159. // - All Topic Names and Topic Filters MUST be at least one character
  160. // long
  161. // - Topic Names and Topic Filters are UTF-8 encoded strings, they MUST
  162. // NOT encode to more than 65535 bytes
  163. if (topicLen < MIN_TOPIC_LEN || topicLen > MAX_TOPIC_LEN) {
  164. throw new IllegalArgumentException(String.format("Invalid topic length, should be in range[%d, %d]!",
  165. new Object[] { Integer.valueOf(MIN_TOPIC_LEN), Integer.valueOf(MAX_TOPIC_LEN) }));
  166. }
  167. // *******************************************************************************
  168. // 1) This is a topic filter string that can contain wildcard characters
  169. // *******************************************************************************
  170. if (wildcardAllowed) {
  171. // Only # or +
  172. if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) {
  173. return;
  174. }
  175. // 1) Check multi-level wildcard
  176. // Rule:
  177. // The multi-level wildcard can be specified only on its own or next
  178. // to the topic level separator character.
  179. // - Can only contains one multi-level wildcard character
  180. // - The multi-level wildcard must be the last character used within
  181. // the topic tree
  182. if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1
  183. || (topicString.contains(MULTI_LEVEL_WILDCARD)
  184. && !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) {
  185. throw new IllegalArgumentException(
  186. "Invalid usage of multi-level wildcard in topic string: " + topicString);
  187. }
  188. // 2) Check single-level wildcard
  189. // Rule:
  190. // The single-level wildcard can be used at any level in the topic
  191. // tree, and in conjunction with the
  192. // multilevel wildcard. It must be used next to the topic level
  193. // separator, except when it is specified on
  194. // its own.
  195. validateSingleLevelWildcard(topicString);
  196. return;
  197. }
  198. // *******************************************************************************
  199. // 2) This is a topic name string that MUST NOT contains any wildcard characters
  200. // *******************************************************************************
  201. if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) {
  202. throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)");
  203. }
  204. }
  205. private static void validateSingleLevelWildcard(String topicString) {
  206. char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0);
  207. char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0);
  208. char[] chars = topicString.toCharArray();
  209. int length = chars.length;
  210. char prev = NUL, next = NUL;
  211. for (int i = 0; i < length; i++) {
  212. prev = (i - 1 >= 0) ? chars[i - 1] : NUL;
  213. next = (i + 1 < length) ? chars[i + 1] : NUL;
  214. if (chars[i] == singleLevelWildcardChar) {
  215. // prev and next can be only '/' or none
  216. if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) {
  217. throw new IllegalArgumentException(
  218. String.format("Invalid usage of single-level wildcard in topic string '%s'!",
  219. new Object[] { topicString }));
  220. }
  221. }
  222. }
  223. }
  224. /**
  225. * Check the supplied topic name and filter match
  226. *
  227. * @param topicFilter
  228. * topic filter: wildcards allowed
  229. * @param topicName
  230. * topic name: wildcards not allowed
  231. * @return true if the topic matches the filter
  232. * @throws IllegalArgumentException
  233. * if the topic name or filter is invalid
  234. */
  235. public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException {
  236. int topicPos = 0;
  237. int filterPos = 0;
  238. int topicLen = topicName.length();
  239. int filterLen = topicFilter.length();
  240. MqttTopic.validate(topicFilter, true);
  241. MqttTopic.validate(topicName, false);
  242. if (topicFilter.equals(topicName)) {
  243. return true;
  244. }
  245. while (filterPos < filterLen && topicPos < topicLen) {
  246. if (topicFilter.charAt(filterPos) == '#') {
  247. /*
  248. * next 'if' will break when topicFilter = topic/# and topicName topic/A/,
  249. * but they are matched
  250. */
  251. topicPos = topicLen;
  252. filterPos = filterLen;
  253. break;
  254. }
  255. if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/')
  256. break;
  257. if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#'
  258. && topicFilter.charAt(filterPos) != topicName.charAt(topicPos))
  259. break;
  260. if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string
  261. int nextpos = topicPos + 1;
  262. while (nextpos < topicLen && topicName.charAt(nextpos) != '/')
  263. nextpos = ++topicPos + 1;
  264. }
  265. filterPos++;
  266. topicPos++;
  267. }
  268. if ((topicPos == topicLen) && (filterPos == filterLen)) {
  269. return true;
  270. } else {
  271. /*
  272. * https://github.com/eclipse/paho.mqtt.java/issues/418
  273. * Covers edge case to match sport/# to sport
  274. */
  275. if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) {
  276. if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#')
  277. return true;
  278. if (topicFilter.length() - filterPos > 1
  279. && topicFilter.substring(filterPos, filterPos + 2).equals("/#")) {
  280. return true;
  281. }
  282. }
  283. }
  284. return false;
  285. }
  286. }