TopicMatcher.java

package net.anyflow.lannister.topic;

import java.io.UnsupportedEncodingException;
import java.util.List;

import com.google.common.base.Strings;

import net.anyflow.lannister.Settings;

public class TopicMatcher {

	private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TopicMatcher.class);

	private static final String SEPARATOR = "/";
	private static final String MULTI_LEVEL_WILDCARD = "#";
	private static final String SINGLE_LEVEL_WILDCARD = "+";

	private static final int MIN_LENGTH = 1;
	private static final int MAX_LENGTH = 65535;
	private static final char NULL = '\u0000';

	private static final int INDEX_NOT_FOUND = -1;

	private static final List<String> BANNED_TOPICFILTERS = Settings.INSTANCE.bannedTopicFilters();

	public static final String MULTI_LEVEL_WILDCARD_PATTERN = SEPARATOR + MULTI_LEVEL_WILDCARD;
	public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD;

	public static boolean isValid(String topicFilter, boolean allowWildcard) {
		if (topicFilter == null) { return false; }
		if (BANNED_TOPICFILTERS.contains(topicFilter)) { return false; }

		int length = 0;
		try {
			length = topicFilter.getBytes("UTF-8").length;
		}
		catch (UnsupportedEncodingException e) {
			logger.error(e.getMessage(), e);
			return false;
		}

		if (length < MIN_LENGTH || length > MAX_LENGTH) {
			logger.error("Invalid topic length [length={}", length);
			return false;
		}

		if (allowWildcard) {
			if (topicFilter.equals(MULTI_LEVEL_WILDCARD) || topicFilter.equals(SINGLE_LEVEL_WILDCARD)) { return true; }

			if (countMatches(topicFilter, MULTI_LEVEL_WILDCARD) > 1 || (topicFilter.contains(MULTI_LEVEL_WILDCARD)
					&& !topicFilter.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) {
				logger.error("Invalid usage of multi-level wildcard in topic string: {}", topicFilter);
				return false;
			}
		}

		return isValidSingleLevelWildcard(topicFilter);
	}

	private static int countMatches(String str, String sub) {
		if (Strings.isNullOrEmpty(str) || Strings.isNullOrEmpty(sub)) { return 0; }

		int count = 0;
		int idx = 0;
		while ((idx = str.indexOf(sub, idx)) != INDEX_NOT_FOUND) {
			count++;
			idx += sub.length();
		}

		return count;
	}

	private static boolean isValidSingleLevelWildcard(String topicString) {
		char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0);
		char topicLevelSeparatorChar = SEPARATOR.charAt(0);

		char[] chars = topicString.toCharArray();
		int length = chars.length;
		char prev = NULL;
		char next = NULL;

		for (int i = 0; i < length; i++) {
			prev = (i - 1 >= 0) ? chars[i - 1] : NULL;
			next = (i + 1 < length) ? chars[i + 1] : NULL;

			if ((chars[i] == singleLevelWildcardChar) && (prev != topicLevelSeparatorChar && prev != NULL
					|| next != topicLevelSeparatorChar && next != NULL)) {
				logger.error("Invalid usage of single-level wildcard in topic string '{}'!", topicString);
				return false;
			}
		}

		return true;
	}

	public static boolean match(String topicFilter, String topicName) {
		if (!TopicMatcher.isValid(topicFilter, true) || !TopicMatcher.isValid(topicName, false)) { return false; }

		if ((topicFilter.startsWith(MULTI_LEVEL_WILDCARD) || topicFilter.startsWith(SINGLE_LEVEL_WILDCARD))
				&& topicName.startsWith("$")) { return false; } // [MQTT-4.7.2-1]

		String[] tfs = topicFilter.split("/");
		String[] tns = topicName.split("/");

		for (int i = 0; i < tns.length; ++i) {
			if (i >= tfs.length) {
				return false;
			}
			else if (tfs[i].equals(MULTI_LEVEL_WILDCARD)) {
				return true;
			}
			else if (tfs[i].equals(SINGLE_LEVEL_WILDCARD)) {
				continue;
			}
			else if (tfs[i].equals(tns[i])) {
				continue;
			}
			else {
				return false;
			}
		}

		return true;
	}
}