Java tutorial
/* * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.wso2.andes.subscription; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.andes.amqp.AMQPUtils; import org.wso2.andes.kernel.AndesException; import org.wso2.andes.kernel.AndesSubscription; import org.wso2.andes.kernel.DestinationType; import org.wso2.andes.kernel.ProtocolType; import org.wso2.andes.mqtt.utils.MQTTUtils; import java.util.*; import java.util.regex.Pattern; /** * Store subscriptions according to the respective protocol using bitmaps as the underlying data structure for * faster wildcard matching. */ public class TopicSubscriptionBitMapStore implements AndesSubscriptionStore { private Log log = LogFactory.getLog(TopicSubscriptionBitMapStore.class); /** * The topic delimiter to differentiate each constituent according to the current subscription type. */ private String constituentsDelimiter; /** * The multi level matching wildcard according to the current subscription type. */ private String multiLevelWildCard; /** * The single level matching wildcard according to the current subscription type. */ private String singleLevelWildCard; private ProtocolType protocolType; // 'Null' and 'Other' constituents are picked from restricted topic characters /** * Constituent name to represent that a constituent is not available at this location. */ private static final String NULL_CONSTITUENT = "%null%"; /** * Constituent name to represent any constituent except a wildcard. */ private static final String OTHER_CONSTITUENT = "%other%"; /** * Keeps all the subscriptions. */ private List<AndesSubscription> subscriptionList = new ArrayList<AndesSubscription>(); /** * Keeps all the subscription destinations broken into their constituents. */ private Map<Integer, String[]> subscriptionConstituents = new HashMap<Integer, String[]>(); /** * Keeps all the constituent tables as ListOfConstituentTables <ConstituentPart, BitSet>. */ private List<Map<String, BitSet>> constituentTables = new ArrayList<Map<String, BitSet>>(); /** * Initialize BitMapHandler with the subscription type. * * @param protocolType The protocol type to handle * @throws AndesException */ public TopicSubscriptionBitMapStore(ProtocolType protocolType) throws AndesException { if (ProtocolType.AMQP == protocolType) { constituentsDelimiter = "."; // AMQPUtils keep wildcard concatenated with constituent delimiter, hence removing them get wildcard only multiLevelWildCard = AMQPUtils.TOPIC_AND_CHILDREN_WILDCARD.replace(constituentsDelimiter, ""); singleLevelWildCard = AMQPUtils.IMMEDIATE_CHILDREN_WILDCARD.replace(constituentsDelimiter, ""); } else if (ProtocolType.MQTT == protocolType) { constituentsDelimiter = "/"; multiLevelWildCard = MQTTUtils.MULTI_LEVEL_WILDCARD; singleLevelWildCard = MQTTUtils.SINGLE_LEVEL_WILDCARD; } else { throw new AndesException("Subscription type " + protocolType + " is not recognized."); } this.protocolType = protocolType; } /** * Add a new subscription to the structure. * * @param subscription The subscription to be added. * @throws AndesException */ @Override public void addSubscription(AndesSubscription subscription) throws AndesException { String destination = subscription.getSubscribedDestination(); if (StringUtils.isNotEmpty(destination)) { if (!isSubscriptionAvailable(subscription)) { int newSubscriptionIndex = subscriptionList.size(); // The index is added to make it clear to which index this is being inserted subscriptionList.add(newSubscriptionIndex, subscription); String constituents[] = destination.split(Pattern.quote(constituentsDelimiter)); subscriptionConstituents.put(newSubscriptionIndex, constituents); for (int constituentIndex = 0; constituentIndex < constituents.length; constituentIndex++) { String constituent = constituents[constituentIndex]; Map<String, BitSet> constituentTable; if ((constituentIndex + 1) > constituentTables.size()) { // No tables exist for this constituent index need to create constituentTable = addConstituentTable(constituentIndex); } else { constituentTable = constituentTables.get(constituentIndex); } if (!constituentTable.keySet().contains(constituent)) { // This constituent is not available in this table. Need to add a new row addConstituentRow(constituent, constituentIndex); } } addSubscriptionColumn(destination, newSubscriptionIndex); } else { updateSubscription(subscription); } } else { throw new AndesException("Error adding a new subscription. Subscribed destination is empty."); } } /** * {@inheritDoc} */ @Override public void updateSubscription(AndesSubscription subscription) { if (isSubscriptionAvailable(subscription)) { // Need to add the new entry to the same index since bitmap logic is dependent on this index int index = subscriptionList.indexOf(subscription); // Should not allow to modify this list until the update is complete // Otherwise the subscription indexes will be invalid synchronized (subscriptionList) { subscriptionList.remove(index); subscriptionList.add(index, subscription); } } } /** * @param constituentIndex The index to create the constituent for * @return The created constituent table */ private Map<String, BitSet> addConstituentTable(int constituentIndex) { Map<String, BitSet> constituentTable = new HashMap<String, BitSet>(); BitSet nullBitSet = new BitSet(subscriptionList.size()); BitSet otherBitSet = new BitSet(subscriptionList.size()); // Fill null and other constituent values for all available subscriptions for (int subscriptionIndex = 0; subscriptionIndex < subscriptionList.size(); subscriptionIndex++) { String[] constituentsOfSubscription = subscriptionConstituents.get(subscriptionIndex); if (constituentsOfSubscription.length < constituentIndex + 1) { // There is no constituent in this subscription for this constituent index nullBitSet.set(subscriptionIndex); // If last constituent of the subscription is multiLevelWildCard, then any other is a match if (multiLevelWildCard.equals(constituentsOfSubscription[constituentsOfSubscription.length - 1])) { otherBitSet.set(subscriptionIndex); } } else { String subscriptionConstituent = constituentsOfSubscription[constituentIndex]; // Check if this is a wildcard if (multiLevelWildCard.equals(subscriptionConstituent) || singleLevelWildCard.equals(subscriptionConstituent)) { otherBitSet.set(subscriptionIndex); } } } // Add 'null' and 'other' constituent constituentTable.put(NULL_CONSTITUENT, nullBitSet); constituentTable.put(OTHER_CONSTITUENT, otherBitSet); constituentTables.add(constituentIndex, constituentTable); return constituentTable; } /** * Run through each constituentTable and insert a new column for a new subscription filling it's values * by comparing constituents. * <p/> * This will only fill values for the already available constituents. Will not add new constituents. * * @param subscribedDestination The newly subscribed destination */ private void addSubscriptionColumn(String subscribedDestination, int subscriptionIndex) throws AndesException { String[] subscribedDestinationConstituents = subscriptionConstituents.get(subscriptionIndex); // Create a mock destination with two constituents for 'other' wildcard matching String matchDestinationForOther = OTHER_CONSTITUENT + constituentsDelimiter + OTHER_CONSTITUENT; // Create a mock destination with three constituents for 'null' wildcard matching String matchDestinationForNull = NULL_CONSTITUENT + constituentsDelimiter + NULL_CONSTITUENT + constituentsDelimiter + NULL_CONSTITUENT; // Loop through each constituent table for the new constituents for (int constituentIndex = 0; constituentIndex < subscribedDestinationConstituents.length; constituentIndex++) { String currentConstituent = subscribedDestinationConstituents[constituentIndex]; Map<String, BitSet> constituentTable = constituentTables.get(constituentIndex); // Loop through each constituent row in the table and fill values for (Map.Entry<String, BitSet> constituentRow : constituentTable.entrySet()) { String constituentOfCurrentRow = constituentRow.getKey(); BitSet bitSet = constituentRow.getValue(); if (constituentOfCurrentRow.equals(currentConstituent)) { bitSet.set(subscriptionIndex); } else if (NULL_CONSTITUENT.equals(constituentOfCurrentRow)) { // Check if this constituent being null matches the destination if we match it with // a null constituent String wildcardDestination = NULL_CONSTITUENT + constituentsDelimiter + currentConstituent; bitSet.set(subscriptionIndex, isMatchForProtocolType(wildcardDestination, matchDestinationForNull)); // } } else if (OTHER_CONSTITUENT.equals(constituentOfCurrentRow)) { // Check if other is matched by comparing wildcard through specific wildcard matching // Create a mock destinations with current constituent added last and check if it match with a // non-wildcard destination match with the corresponding matching method String wildCardDestination = OTHER_CONSTITUENT + constituentsDelimiter + currentConstituent; bitSet.set(subscriptionIndex, isMatchForProtocolType(wildCardDestination, matchDestinationForOther)); } else if (singleLevelWildCard.equals(currentConstituent) || multiLevelWildCard.equals(currentConstituent)) { // If there is any wildcard at this position, then this should match. bitSet.set(subscriptionIndex); } else { bitSet.set(subscriptionIndex, false); } } } int noOfMaxConstituents = constituentTables.size(); if (noOfMaxConstituents > subscribedDestinationConstituents.length) { // There are more constituent tables to be filled. Wildcard matching is essential here. boolean matchingOthers = true; // The OTHER_CONSTITUENT is added here to represent any constituent if (!multiLevelWildCard .equals(subscribedDestinationConstituents[subscribedDestinationConstituents.length - 1])) { String otherConstituentComparer = subscribedDestination + constituentsDelimiter + OTHER_CONSTITUENT; matchingOthers = isMatchForProtocolType(subscribedDestination, otherConstituentComparer); } // Else matchingOthers will be true for (int constituentIndex = subscribedDestinationConstituents.length; constituentIndex < noOfMaxConstituents; constituentIndex++) { Map<String, BitSet> constituentTable = constituentTables.get(constituentIndex); // Loop through each constituent row in the table and fill values for (Map.Entry<String, BitSet> constituentRow : constituentTable.entrySet()) { String constituentOfCurrentRow = constituentRow.getKey(); BitSet bitSet = constituentRow.getValue(); if (NULL_CONSTITUENT.equals(constituentOfCurrentRow)) { // Null constituent is always true here bitSet.set(subscriptionIndex); } else { bitSet.set(subscriptionIndex, matchingOthers); } } } } } /** * Add a new constituent row for the given constituent index table and fill values for already available * subscriptions. * * @param constituent The constituent to add * @param constituentIndex The index of the constituent */ private void addConstituentRow(String constituent, int constituentIndex) { Map<String, BitSet> constituentTable = constituentTables.get(constituentIndex); BitSet bitSet = new BitSet(); for (int i = 0; i < subscriptionConstituents.size(); i++) { String[] constituentsOfSubscription = subscriptionConstituents.get(i); if (constituentIndex < constituentsOfSubscription.length) { // Get the i'th subscription's [constituentIndex]'th constituent String subscriptionConstituent = constituentsOfSubscription[constituentIndex]; if (subscriptionConstituent.equals(constituent) || multiLevelWildCard.equals(subscriptionConstituent) || singleLevelWildCard.equals(subscriptionConstituent)) { // The new constituent matches the subscriptions i'th constituent bitSet.set(i); } else { // The new constituent does not match the i'th subscriptions [constituentIndex] constituent bitSet.set(i, false); } } else { // The subscription does not have a constituent for this index // If the last constituent of the subscription is multiLevelWildCard we match else false if (multiLevelWildCard.equals(constituentsOfSubscription[constituentsOfSubscription.length - 1])) { bitSet.set(i); } else { bitSet.set(i, false); } } } constituentTable.put(constituent, bitSet); } /** * Return the match between the given two parameters with respect to the protocol. * * @param wildCardDestination The destination with/without wildcard * @param nonWildCardDestination The direct destination without wildcards * @return Match status * @throws AndesException */ private boolean isMatchForProtocolType(String wildCardDestination, String nonWildCardDestination) throws AndesException { boolean matching = false; if (ProtocolType.AMQP == protocolType) { matching = AMQPUtils.isTargetQueueBoundByMatchingToRoutingKey(wildCardDestination, nonWildCardDestination); } else if (ProtocolType.MQTT == protocolType) { matching = MQTTUtils.isTargetQueueBoundByMatchingToRoutingKey(wildCardDestination, nonWildCardDestination); } else { throw new AndesException("Protocol type " + protocolType + " is not recognized."); } return matching; } /** * This methods adds a constituent table with only null and other constituents. * This is required when a message comes with more than the available number of constituents. If wildcard * subscriptions are available for those, they should match. Hence need to create these empty constituent tables. */ private void addEmptyConstituentTable() { int noOfSubscriptions = subscriptionList.size(); Map<String, BitSet> constituentTable = new HashMap<String, BitSet>(); BitSet nullBitSet = new BitSet(noOfSubscriptions); BitSet otherBitSet = new BitSet(noOfSubscriptions); if (noOfSubscriptions > 0) { // Null constituent will always be true for empty constituents, hence need to flip nullBitSet.flip(0, noOfSubscriptions - 1); for (int subscriptionIndex = 0; subscriptionIndex < noOfSubscriptions; subscriptionIndex++) { // For 'other', if subscribers last constituent is multi level wild card then matching String[] allConstituent = subscriptionConstituents.get(subscriptionIndex); String lastConstituent = allConstituent[allConstituent.length - 1]; if (multiLevelWildCard.equals(lastConstituent)) { otherBitSet.set(subscriptionIndex); } else { otherBitSet.set(subscriptionIndex, false); } } } constituentTable.put(NULL_CONSTITUENT, nullBitSet); constituentTable.put(OTHER_CONSTITUENT, otherBitSet); constituentTables.add(constituentTable); } /** * Removing a subscription from the structure. * * @param subscription The subscription to remove */ @Override public void removeSubscription(AndesSubscription subscription) { int subscriptionIndex = subscriptionList.indexOf(subscription); if (subscriptionIndex > -1) { for (Map<String, BitSet> constituentTable : constituentTables) { for (Map.Entry<String, BitSet> constituentRow : constituentTable.entrySet()) { // For every row create a new BitSet with the values for the removed subscription removed String constituent = constituentRow.getKey(); BitSet bitSet = constituentRow.getValue(); BitSet newBitSet = new BitSet(); int bitIndex = 0; for (int i = 0; i < bitSet.size(); i++) { if (bitIndex == i) { // If the this is the index to remove then skip this round bitIndex++; } newBitSet.set(i, bitSet.get(bitIndex)); bitIndex++; } constituentTable.put(constituent, newBitSet); } } // Remove the subscription from subscription list subscriptionList.remove(subscriptionIndex); } else { log.warn("Subscription for destination : " + subscription.getSubscribedDestination() + " is not found to " + "remove"); } } /** * {@inheritDoc} */ @Override public boolean isSubscriptionAvailable(AndesSubscription subscription) { return subscriptionList.contains(subscription); } /** * {@inheritDoc} */ @Override public Set<AndesSubscription> getMatchingSubscriptions(String destination, DestinationType destinationType) { Set<AndesSubscription> subscriptions = new HashSet<>(); if (StringUtils.isNotEmpty(destination)) { // constituentDelimiter is quoted to avoid making the delimiter a regex symbol String[] constituents = destination.split(Pattern.quote(constituentsDelimiter), -1); int noOfCurrentMaxConstituents = constituentTables.size(); // If given destination has more constituents than any subscriber has, then create constituent tables // for those before collecting matching subscribers if (constituents.length > noOfCurrentMaxConstituents) { for (int i = noOfCurrentMaxConstituents; i < constituents.length; i++) { addEmptyConstituentTable(); } } // Keeps the results of 'AND' operations between each bit sets BitSet andBitSet = new BitSet(subscriptionList.size()); // Since BitSet is initialized with false for each element we need to flip andBitSet.flip(0, subscriptionList.size()); // Get corresponding bit set for each constituent in the destination and operate bitwise AND operation for (int constituentIndex = 0; constituentIndex < constituents.length; constituentIndex++) { String constituent = constituents[constituentIndex]; Map<String, BitSet> constituentTable = constituentTables.get(constituentIndex); BitSet bitSetForAnd = constituentTable.get(constituent); if (null == bitSetForAnd) { // The constituent is not found in the table, hence matching with 'other' constituent bitSetForAnd = constituentTable.get(OTHER_CONSTITUENT); } andBitSet.and(bitSetForAnd); } // If there are more constituent tables, get the null constituent in each of them and operate bitwise AND for (int constituentIndex = constituents.length; constituentIndex < constituentTables .size(); constituentIndex++) { Map<String, BitSet> constituentTable = constituentTables.get(constituentIndex); andBitSet.and(constituentTable.get(NULL_CONSTITUENT)); } // Valid subscriptions are filtered, need to pick from subscription pool int nextSetBitIndex = andBitSet.nextSetBit(0); while (nextSetBitIndex > -1) { subscriptions.add(subscriptionList.get(nextSetBitIndex)); nextSetBitIndex = andBitSet.nextSetBit(nextSetBitIndex + 1); } } else { log.warn("Cannot retrieve subscriptions via bitmap handler since destination to match is empty"); } return subscriptions; } /** * Get all the subscriptions currently saved. * * @return List of all subscriptions */ @Override public List<AndesSubscription> getAllSubscriptions() { return subscriptionList; } /** * {@inheritDoc} */ public Set<String> getAllDestinations(DestinationType destinationType) { Set<String> topics = new HashSet<>(); for (Map.Entry<Integer, String[]> subcriberConstituent : subscriptionConstituents.entrySet()) { StringBuilder topic = new StringBuilder(); String[] constituents = subcriberConstituent.getValue(); for (int i = 0; i < constituents.length; i++) { String constituent = constituents[i]; // if this is a wildcard constituent, we provide it as 'ANY' in it's place for readability if (multiLevelWildCard.equals(constituent) || singleLevelWildCard.equals(constituent)) { topic.append("ANY"); } else { topic.append(constituent); } // append the delimiter if there are more constituents to come if ((constituents.length - 1) > i) { topic.append(constituentsDelimiter); } } topics.add(topic.toString()); } return topics; } }