com.comcast.cns.persistence.CNSSubscriptionCassandraPersistence.java Source code

Java tutorial

Introduction

Here is the source code for com.comcast.cns.persistence.CNSSubscriptionCassandraPersistence.java

Source

/**
 * Copyright 2012 Comcast Corporation
 * 
 * Licensed 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 com.comcast.cns.persistence;

import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONWriter;

import com.comcast.cmb.common.persistence.AbstractDurablePersistence;
import com.comcast.cmb.common.persistence.AbstractDurablePersistence.CMB_SERIALIZER;
import com.comcast.cmb.common.persistence.AbstractDurablePersistence.CmbColumn;
import com.comcast.cmb.common.persistence.AbstractDurablePersistence.CmbColumnSlice;
import com.comcast.cmb.common.persistence.AbstractDurablePersistence.CmbComposite;
import com.comcast.cmb.common.persistence.DurablePersistenceFactory;
import com.comcast.cmb.common.persistence.PersistenceFactory;
import com.comcast.cmb.common.util.CMBErrorCodes;
import com.comcast.cmb.common.util.CMBException;
import com.comcast.cmb.common.util.CMBProperties;
import com.comcast.cmb.common.util.PersistenceException;
import com.comcast.cns.model.CNSSubscription;
import com.comcast.cns.model.CNSSubscriptionAttributes;
import com.comcast.cns.model.CNSTopic;
import com.comcast.cns.model.CNSSubscription.CnsSubscriptionProtocol;
import com.comcast.cns.util.Util;
import com.comcast.cqs.model.CQSQueue;

/**
 * 
 * Column-name for CNSTopicSubscriptions table is composite(Endpoint, Protocol). row-key is topicARn
 * @author aseem, bwolf, vvenkatraman, tina, jorge
 * 
 * Class is immutable
 */
public class CNSSubscriptionCassandraPersistence implements ICNSSubscriptionPersistence {

    private static Logger logger = Logger.getLogger(CNSSubscriptionCassandraPersistence.class);

    private static final String columnFamilySubscriptions = "CNSTopicSubscriptions";
    private static final String columnFamilySubscriptionsIndex = "CNSTopicSubscriptionsIndex";
    private static final String columnFamilySubscriptionsUserIndex = "CNSTopicSubscriptionsUserIndex";
    private static final String columnFamilySubscriptionsTokenIndex = "CNSTopicSubscriptionsTokenIndex";
    private static final String columnFamilyTopicStats = "CNSTopicStats";

    private static final AbstractDurablePersistence cassandraHandler = DurablePersistenceFactory.getInstance();

    public CNSSubscriptionCassandraPersistence() {
    }

    @SuppressWarnings("serial")
    private Map<String, String> getIndexColumnValues(final String endpoint,
            final CnsSubscriptionProtocol protocol) {
        return new HashMap<String, String>() {
            {
                put(getEndpointAndProtoIndexVal(endpoint, protocol), "");
            }
        };
    }

    private String getColumnValuesJSON(CNSSubscription s) throws JSONException {

        Writer writer = new StringWriter();
        JSONWriter jw = new JSONWriter(writer);
        jw = jw.object();

        if (s.getEndpoint() != null) {
            jw.key("endPoint").value(s.getEndpoint());
        }

        if (s.getToken() != null) {
            jw.key("token").value(s.getToken());
        }

        if (s.getArn() != null) {
            jw.key("subArn").value(s.getArn());
        }

        if (s.getUserId() != null) {
            jw.key("userId").value(s.getUserId());
        }

        if (s.getConfirmDate() != null) {
            jw.key("confirmDate").value(s.getConfirmDate().getTime() + "");
        }

        if (s.getProtocol() != null) {
            jw.key("protocol").value(s.getProtocol().toString());
        }

        if (s.getRequestDate() != null) {
            jw.key("requestDate").value(s.getRequestDate().getTime() + "");
        }

        jw.key("authenticateOnSubscribe").value(s.isAuthenticateOnUnsubscribe() + "");
        jw.key("isConfirmed").value(s.isConfirmed() + "");
        jw.key("rawMessageDelivery").value(s.getRawMessageDelivery() + "");

        jw.endObject();

        return writer.toString();
    }

    /*private Map<String, String> getColumnValues(CNSSubscription s) {
           
       Map<String, String> columnValues = new HashMap<String, String>();
           
       if (s.getEndpoint() != null) {
     columnValues.put("endPoint", s.getEndpoint());
       }
           
       if (s.getToken() != null) {
     columnValues.put("token", s.getToken());
       }
           
       if (s.getArn() != null) {
      columnValues.put("subArn", s.getArn());
       }
           
       if (s.getUserId() != null) {
     columnValues.put("userId", s.getUserId());
       }
           
       if (s.getConfirmDate() != null) {
     columnValues.put("confirmDate", s.getConfirmDate().getTime() + "");
       }
           
       if (s.getProtocol() != null) {
     columnValues.put("protocol", s.getProtocol().toString());
       }
           
       if (s.getRequestDate() != null) {
     columnValues.put("requestDate", s.getRequestDate().getTime() + "");
       }
           
       columnValues.put("authenticateOnSubscribe", s.isAuthenticateOnUnsubscribe() + "");
       columnValues.put("isConfirmed", s.isConfirmed() + "");
       columnValues.put("rawMessageDelivery", s.getRawMessageDelivery() + "");
        
       return columnValues;
    }*/

    /*private static CNSSubscription getSubscriptionFromMap(Map<String, String> map) {
           
        String subArn = map.get("subArn");
            
        CNSSubscription s = new CNSSubscription(subArn);
            
        s.setEndpoint(map.get("endPoint"));
        s.setUserId(map.get("userId"));
            
        String confirmDate = map.get("confirmDate");
            
        if (confirmDate != null) {
      s.setConfirmDate(new Date(Long.parseLong(confirmDate)));
        }
            
    String requestDate = map.get("requestDate");
        
    if (requestDate != null) {
       s.setRequestDate(new Date(Long.parseLong(requestDate)));
    }
        
    String protocol = map.get("protocol");
        
    if (protocol != null) {
       s.setProtocol(CnsSubscriptionProtocol.valueOf(protocol));
    }
        
    String isConfirmed = map.get("isConfirmed");
        
    if (isConfirmed != null) {
       s.setConfirmed(Boolean.parseBoolean(isConfirmed));
    }
        
    s.setToken(map.get("token"));
        
    String authenticateOnSubscribe = map.get("authenticateOnSubscribe");
        
    if (authenticateOnSubscribe != null) {
       s.setAuthenticateOnUnsubscribe(Boolean.parseBoolean(authenticateOnSubscribe));
    }
        
    String rawMessage = map.get("rawMessageDelivery");
    if (rawMessage != null) {
       s.setRawMessageDelivery(Boolean.parseBoolean(rawMessage));
    }
        
    return s;
    }*/

    private static String getEndpointAndProtoIndexVal(String endpoint, CnsSubscriptionProtocol protocol) {
        return protocol.name() + ":" + endpoint;
    }

    private static String getEndpointAndProtoIndexValEndpoint(String composite) {
        String[] arr = composite.split(":");
        if (arr.length < 2) {
            throw new IllegalArgumentException(
                    "Bad format for EndpointAndProtocol composite. Must be of the form <protocol>:<endpoint>. Got:"
                            + composite);
        }
        StringBuffer sb = new StringBuffer(arr[1]);
        for (int i = 2; i < arr.length; i++) {
            sb.append(":").append(arr[i]);
        }
        return sb.toString();
    }

    private static CnsSubscriptionProtocol getEndpointAndProtoIndexValProtocol(String composite) {
        String[] arr = composite.split(":");
        if (arr.length < 2) {
            throw new IllegalArgumentException(
                    "Bad format for EndpointAndProtocol composite. Must be of the form <protocol>:<endpoint>. Got:"
                            + composite);
        }
        return CnsSubscriptionProtocol.valueOf(arr[0]);
    }

    private void insertOrUpdateSubsAndIndexes(final CNSSubscription subscription, Integer ttl) throws Exception {
        subscription.checkIsValid();
        CmbComposite columnName = cassandraHandler.getCmbComposite(subscription.getEndpoint(),
                subscription.getProtocol().name());
        cassandraHandler.update(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptions,
                subscription.getTopicArn(), columnName, getColumnValuesJSON(subscription),
                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.COMPOSITE_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER, ttl);
        cassandraHandler.insertRow(AbstractDurablePersistence.CNS_KEYSPACE, subscription.getArn(),
                columnFamilySubscriptionsIndex,
                getIndexColumnValues(subscription.getEndpoint(), subscription.getProtocol()),
                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER, ttl);
        cassandraHandler.insertRow(AbstractDurablePersistence.CNS_KEYSPACE, subscription.getUserId(),
                columnFamilySubscriptionsUserIndex, new HashMap<String, String>() {
                    {
                        put(subscription.getArn(), "");
                    }
                }, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER, ttl);
        cassandraHandler.insertRow(AbstractDurablePersistence.CNS_KEYSPACE, subscription.getToken(),
                columnFamilySubscriptionsTokenIndex, new HashMap<String, String>() {
                    {
                        put(subscription.getArn(), "");
                    }
                }, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER, ttl);
    }

    @Override
    public CNSSubscription subscribe(String endpoint, CnsSubscriptionProtocol protocol, String topicArn,
            String userId) throws Exception {

        // subscription is unique by protocol + endpoint + topic

        final CNSSubscription subscription = new CNSSubscription(endpoint, protocol, topicArn, userId);

        CNSTopic t = PersistenceFactory.getTopicPersistence().getTopic(topicArn);

        if (t == null) {
            throw new TopicNotFoundException("Resource not found.");
        }

        // check if queue exists for cqs endpoints

        if (protocol.equals(CnsSubscriptionProtocol.cqs)) {

            CQSQueue queue = PersistenceFactory.getQueuePersistence()
                    .getQueue(com.comcast.cqs.util.Util.getRelativeQueueUrlForArn(endpoint));

            if (queue == null) {
                throw new CMBException(CMBErrorCodes.NotFound, "Queue with arn " + endpoint + " does not exist.");
            }
        }

        subscription.setArn(Util.generateCnsTopicSubscriptionArn(topicArn, protocol, endpoint));

        // attempt to delete existing subscription

        /*Composite superColumnName = new Composite(subscription.getEndpoint(), subscription.getProtocol().name());
            
        HSuperColumn<Composite, String, String> superCol = readColumnFromSuperColumnFamily(columnFamilySubscriptions, subscription.getTopicArn(), superColumnName, new StringSerializer(), new CompositeSerializer(), StringSerializer.get(), StringSerializer.get(), CMBProperties.getInstance().getReadConsistencyLevel());
            
        if (superCol != null) {
           CNSSubscription exisitingSub = extractSubscriptionFromSuperColumn(superCol, topicArn);
        deleteIndexes(exisitingSub.getArn(), exisitingSub.getUserId(), exisitingSub.getToken());
           deleteSuperColumn(subscriptionsTemplate, exisitingSub.getTopicArn(), superColumnName);
        }*/

        // then set confirmation stuff and update cassandra

        CNSSubscription retrievedSubscription = getSubscription(subscription.getArn());

        if (!CMBProperties.getInstance().getCNSRequireSubscriptionConfirmation()) {

            subscription.setConfirmed(true);
            subscription.setConfirmDate(new Date());

            insertOrUpdateSubsAndIndexes(subscription, null);

            if (retrievedSubscription == null) {
                cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                        subscription.getTopicArn(), "subscriptionConfirmed", 1, CMB_SERIALIZER.STRING_SERIALIZER,
                        CMB_SERIALIZER.STRING_SERIALIZER);
            }

        } else {

            // protocols that cannot confirm subscriptions (e.g. redisPubSub)
            // get an automatic confirmation here
            if (!protocol.canConfirmSubscription()) {
                subscription.setConfirmed(true);
                subscription.setConfirmDate(new Date());
                insertOrUpdateSubsAndIndexes(subscription, null);

                // auto confirm subscription to cqs queue by owner
            } else if (protocol.equals(CnsSubscriptionProtocol.cqs)) {

                String queueOwner = com.comcast.cqs.util.Util.getQueueOwnerFromArn(endpoint);

                if (queueOwner != null && queueOwner.equals(userId)) {

                    subscription.setConfirmed(true);
                    subscription.setConfirmDate(new Date());

                    insertOrUpdateSubsAndIndexes(subscription, null);
                    if (retrievedSubscription == null) {
                        cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE,
                                columnFamilyTopicStats, subscription.getTopicArn(), "subscriptionConfirmed", 1,
                                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);

                    }
                } else {

                    // use cassandra ttl to implement expiration after 3 days 
                    insertOrUpdateSubsAndIndexes(subscription, 3 * 24 * 60 * 60);
                    if (retrievedSubscription == null) {
                        cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE,
                                columnFamilyTopicStats, subscription.getTopicArn(), "subscriptionPending", 1,
                                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
                    }
                }

            } else {

                // use cassandra ttl to implement expiration after 3 days 
                insertOrUpdateSubsAndIndexes(subscription, 3 * 24 * 60 * 60);
                if (retrievedSubscription == null) {
                    cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE,
                            columnFamilyTopicStats, subscription.getTopicArn(), "subscriptionPending", 1,
                            CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
                }
            }
        }

        CNSSubscriptionAttributes attributes = new CNSSubscriptionAttributes(topicArn, subscription.getArn(),
                userId);
        PersistenceFactory.getCNSAttributePersistence().setSubscriptionAttributes(attributes,
                subscription.getArn());

        return subscription;
    }

    @Override
    public CNSSubscription getSubscription(String arn) throws Exception {

        //read form index to get composite col-name

        CmbColumnSlice<String, String> slice = cassandraHandler.readColumnSlice(
                AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsIndex, arn, null, null, 1,
                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);

        if (slice != null) {

            //get Column from main table

            String colName = slice.getColumns().get(0).getName();
            CnsSubscriptionProtocol protocol = getEndpointAndProtoIndexValProtocol(colName);
            String endpoint = getEndpointAndProtoIndexValEndpoint(colName);
            CmbComposite columnName = cassandraHandler.getCmbComposite(endpoint, protocol.name());
            CmbColumn<CmbComposite, String> column = cassandraHandler.readColumn(
                    AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptions, Util.getCnsTopicArn(arn),
                    columnName, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.COMPOSITE_SERIALIZER,
                    CMB_SERIALIZER.STRING_SERIALIZER);

            if (column != null) {
                CNSSubscription s = extractSubscriptionFromColumn(column, Util.getCnsTopicArn(arn));
                s.checkIsValid();
                return s;
            }
        }

        return null;
    }

    private static CNSSubscription extractSubscriptionFromColumn(CmbColumn<CmbComposite, String> column,
            String topicArn) throws JSONException {

        JSONObject json = new JSONObject(column.getValue());
        CNSSubscription s = new CNSSubscription(json.getString("subArn"));

        s.setEndpoint(json.getString("endPoint"));
        s.setUserId(json.getString("userId"));

        if (json.has("confirmDate")) {
            s.setConfirmDate(new Date(json.getLong("confirmDate")));
        }

        if (json.has("requestDate")) {
            s.setRequestDate(new Date(json.getLong("requestDate")));
        }

        if (json.has("protocol")) {
            s.setProtocol(CnsSubscriptionProtocol.valueOf(json.getString("protocol")));
        }

        if (json.has("isConfirmed")) {
            s.setConfirmed(json.getBoolean("isConfirmed"));
        }

        s.setToken(json.getString("token"));

        if (json.has("authenticateOnSubscribe")) {
            s.setAuthenticateOnUnsubscribe(json.getBoolean("authenticateOnSubscribe"));
        }

        if (json.has("rawMessageDelivery")) {
            s.setRawMessageDelivery(json.getBoolean("rawMessageDelivery"));
        }

        s.setTopicArn(topicArn);

        return s;
    }

    /*private static CNSSubscription extractSubscriptionFromSuperColumn(CmbSuperColumn<CmbComposite, String, String> superCol, String topicArn) {
            
        Map<String, String> messageMap = new HashMap<String, String>(superCol.getColumns().size());
            
    for (CmbColumn<String, String> column : superCol.getColumns()) {
        messageMap.put(column.getName(), column.getValue());
    }
        
    CNSSubscription sub = getSubscriptionFromMap(messageMap);
    sub.setTopicArn(topicArn);
        
    return sub;
    }*/

    @Override
    /**
     * List all subscription for a user, unconfirmed subscriptions will not reveal their arns. Pagination for more than 100 subscriptions.
     * @param nextToken initially null, on subsequent calls arn of last result from prior call
     * @param protocol optional filter by protocol (this parameter is not part of official AWS API)
     * @return list of subscriptions. If nextToken is not null, the subscription corresponding to it is not returned.
     * @throws Exception
     */
    public List<CNSSubscription> listSubscriptions(String nextToken, CnsSubscriptionProtocol protocol,
            String userId) throws Exception {
        return listSubscriptions(nextToken, protocol, userId, true);
    }

    private List<CNSSubscription> listSubscriptions(String nextToken, CnsSubscriptionProtocol protocol,
            String userId, boolean hidePendingArn) throws Exception {

        if (nextToken != null) {
            if (getSubscription(nextToken) == null) {
                throw new SubscriberNotFoundException("Subscriber not found for arn " + nextToken);
            }
        }

        //Algorithm is to keep reading in chunks of 100 till we've seen 500 from the nextToken-ARN         
        List<CNSSubscription> l = new ArrayList<CNSSubscription>();
        //read form index to get sub-arn        

        while (l.size() < 100) {

            CmbColumnSlice<String, String> slice = cassandraHandler.readColumnSlice(
                    AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsUserIndex, userId, nextToken,
                    null, 500, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                    CMB_SERIALIZER.STRING_SERIALIZER);

            if (slice == null) {
                return l;
            }

            //get Column from main table
            List<CmbColumn<String, String>> cols = slice.getColumns();

            if (nextToken != null) {
                cols.remove(0);
            }

            if (cols.size() == 0) {
                return l;
            }

            for (CmbColumn<String, String> col : cols) {

                String subArn = col.getName();
                CNSSubscription subscription = getSubscription(subArn);

                if (subscription == null) {
                    throw new IllegalStateException(
                            "Subscriptions-user-index contains subscription-arn which doesn't exist in subscriptions-index. subArn:"
                                    + subArn);
                }

                // ignore invalid subscriptions coming from Cassandra

                try {
                    subscription.checkIsValid();
                } catch (CMBException ex) {
                    logger.error("event=invalid_subscription " + subscription.toString(), ex);
                    continue;
                }

                if (protocol != null && subscription.getProtocol() != protocol) {
                    continue;
                }

                if (hidePendingArn) {
                    if (subscription.isConfirmed()) {
                        l.add(subscription);
                    } else {
                        subscription.setArn("PendingConfirmation");
                        l.add(subscription);
                    }
                } else {
                    l.add(subscription);
                }

                if (l.size() == 100) {
                    return l;
                }
            }

            nextToken = cols.get(cols.size() - 1).getName();
        }

        return l;
    }

    @Override
    public List<CNSSubscription> listAllSubscriptions(String nextToken, CnsSubscriptionProtocol protocol,
            String userId) throws Exception {
        return listSubscriptions(nextToken, protocol, userId, false);
    }

    @Override
    public List<CNSSubscription> listSubscriptionsByTopic(String nextToken, String topicArn,
            CnsSubscriptionProtocol protocol) throws Exception {
        return listSubscriptionsByTopic(nextToken, topicArn, protocol, 100);
    }

    @Override
    public List<CNSSubscription> listSubscriptionsByTopic(String nextToken, String topicArn,
            CnsSubscriptionProtocol protocol, int pageSize) throws Exception {
        return listSubscriptionsByTopic(nextToken, topicArn, protocol, pageSize, true);
    }

    /**
     * Enumerate all subs in a topic
     * @param nextToken The ARN of the last sub-returned or null is first time call.
     * @param topicArn
     * @param protocol
     * @param pageSize
     * @param hidePendingArn
     * @return The list of subscriptions given a topic. Note: if nextToken is provided, the returned list will not contain it for convenience
     * @throws Exception
     */
    public List<CNSSubscription> listSubscriptionsByTopic(String nextToken, String topicArn,
            CnsSubscriptionProtocol protocol, int pageSize, boolean hidePendingArn) throws Exception {

        if (nextToken != null) {
            if (getSubscription(nextToken) == null) {
                throw new SubscriberNotFoundException("Subscriber not found for arn " + nextToken);
            }
        }

        //read from index to get composite-col-name corresponding to nextToken
        CmbComposite nextTokenComposite = null;

        if (nextToken != null) {
            CmbColumnSlice<String, String> slice = cassandraHandler.readColumnSlice(
                    AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsIndex, nextToken, null, null,
                    1, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                    CMB_SERIALIZER.STRING_SERIALIZER);
            if (slice == null) {
                throw new IllegalArgumentException("Could not find any subscription with arn " + nextToken);
            }
            //get Column from main table
            String colName = slice.getColumns().get(0).getName();
            CnsSubscriptionProtocol tokProtocol = getEndpointAndProtoIndexValProtocol(colName);
            String endpoint = getEndpointAndProtoIndexValEndpoint(colName);
            nextTokenComposite = cassandraHandler.getCmbComposite(endpoint, tokProtocol.name());
        }

        List<CNSSubscription> l = new ArrayList<CNSSubscription>();

        CNSTopic t = PersistenceFactory.getTopicPersistence().getTopic(topicArn);

        if (t == null) {
            throw new TopicNotFoundException("Resource not found.");
        }

        // read pageSize at a time

        CmbColumnSlice<CmbComposite, String> cols = cassandraHandler.readColumnSlice(
                AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptions, topicArn, nextTokenComposite,
                null, pageSize, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.COMPOSITE_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);

        if (nextToken != null && cols.size() > 0) {
            cols.getColumns().remove(0);
        }

        while (l.size() < pageSize) {

            if (cols == null || cols.size() == 0) {
                return l;
            }

            for (CmbColumn<CmbComposite, String> col : cols.getColumns()) {

                CNSSubscription sub = extractSubscriptionFromColumn(col, topicArn);

                // ignore invalid subscriptions coming from Cassandra

                try {
                    sub.checkIsValid();
                } catch (CMBException ex) {
                    logger.error("event=invalid_subscription " + sub.toString(), ex);
                    continue;
                }

                if (protocol != null && protocol != sub.getProtocol()) {
                    continue;
                }

                if (hidePendingArn) {
                    if (sub.isConfirmed()) {
                        l.add(sub);
                    } else {
                        sub.setArn("PendingConfirmation");
                        l.add(sub);
                    }
                } else {
                    l.add(sub);
                }

                if (l.size() == pageSize) {
                    return l;
                }
            }

            nextTokenComposite = cols.getColumns().get(cols.size() - 1).getName();
            cols = cassandraHandler.readColumnSlice(AbstractDurablePersistence.CNS_KEYSPACE,
                    columnFamilySubscriptions, topicArn, nextTokenComposite, null, pageSize,
                    CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.COMPOSITE_SERIALIZER,
                    CMB_SERIALIZER.STRING_SERIALIZER);

            if (cols.size() > 0) {
                cols.getColumns().remove(0);
            }
        }

        return l;
    }

    @Override
    public List<CNSSubscription> listAllSubscriptionsByTopic(String nextToken, String topicArn,
            CnsSubscriptionProtocol protocol) throws Exception {
        return listSubscriptionsByTopic(nextToken, topicArn, protocol, 100, false);
    }

    @Override
    public CNSSubscription confirmSubscription(boolean authenticateOnUnsubscribe, String token, String topicArn)
            throws Exception {

        //get Sub-arn given token
        CmbColumnSlice<String, String> slice = cassandraHandler.readColumnSlice(
                AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsTokenIndex, token, null, null, 1,
                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);

        if (slice == null) {
            throw new CMBException(CMBErrorCodes.NotFound, "Resource not found.");
        }

        //get Column from main table
        String subArn = slice.getColumns().get(0).getName();

        //get Subscription given subArn
        final CNSSubscription s = getSubscription(subArn);

        if (s == null) {
            throw new SubscriberNotFoundException("Could not find subscription given subscription arn " + subArn);
        }

        s.setAuthenticateOnUnsubscribe(authenticateOnUnsubscribe);
        s.setConfirmed(true);
        s.setConfirmDate(new Date());

        //re-insert with no TTL. will clobber the old one which had ttl
        insertOrUpdateSubsAndIndexes(s, null);

        cassandraHandler.decrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                s.getTopicArn(), "subscriptionPending", 1, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);
        cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                s.getTopicArn(), "subscriptionConfirmed", 1, CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);

        return s;
    }

    private void deleteIndexesAll(List<CNSSubscription> subscriptionList) throws PersistenceException {
        List<String> subArnList = new LinkedList<String>();
        List<String> userIdList = new LinkedList<String>();
        List<String> tokenList = new LinkedList<String>();
        for (CNSSubscription sub : subscriptionList) {
            subArnList.add(sub.getArn());
            userIdList.add(sub.getUserId());
            tokenList.add(sub.getToken());
        }
        cassandraHandler.deleteBatch(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsIndex,
                subArnList, null, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
        cassandraHandler.deleteBatch(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsUserIndex,
                userIdList, subArnList, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
        cassandraHandler.deleteBatch(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsTokenIndex,
                tokenList, subArnList, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
    }

    private void deleteIndexes(String subArn, String userId, String token) throws PersistenceException {
        cassandraHandler.delete(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsIndex, subArn,
                null, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
        cassandraHandler.delete(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsUserIndex, userId,
                subArn, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
        cassandraHandler.delete(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptionsTokenIndex, token,
                subArn, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
    }

    @Override
    public void unsubscribe(String arn) throws Exception {

        CNSSubscription s = getSubscription(arn);

        if (s != null) {

            deleteIndexes(arn, s.getUserId(), s.getToken());
            CmbComposite columnName = cassandraHandler.getCmbComposite(s.getEndpoint(), s.getProtocol().name());
            cassandraHandler.delete(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptions,
                    Util.getCnsTopicArn(arn), columnName, CMB_SERIALIZER.STRING_SERIALIZER,
                    CMB_SERIALIZER.COMPOSITE_SERIALIZER);

            if (s.isConfirmed()) {
                cassandraHandler.decrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                        s.getTopicArn(), "subscriptionConfirmed", 1, CMB_SERIALIZER.STRING_SERIALIZER,
                        CMB_SERIALIZER.STRING_SERIALIZER);
            } else {
                cassandraHandler.decrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                        s.getTopicArn(), "subscriptionPending", 1, CMB_SERIALIZER.STRING_SERIALIZER,
                        CMB_SERIALIZER.STRING_SERIALIZER);
            }

            cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                    s.getTopicArn(), "subscriptionDeleted", 1, CMB_SERIALIZER.STRING_SERIALIZER,
                    CMB_SERIALIZER.STRING_SERIALIZER);
        }
    }

    public long getCountSubscription(String topicArn, String columnName) throws Exception {
        return cassandraHandler.getCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                topicArn, columnName, CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
    }

    @Override
    public void unsubscribeAll(String topicArn) throws Exception {

        int pageSize = 1000;

        String nextToken = null;
        List<CNSSubscription> subs = listSubscriptionsByTopic(nextToken, topicArn, null, pageSize, false);

        // for pagination to work we need the nextToken's corresponding sub to not be deleted.

        CNSSubscription nextTokenSub = null;

        while (subs.size() > 0) {

            if (subs.size() < pageSize) {
                deleteIndexesAll(subs);
                cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                        topicArn, "subscriptionDeleted", subs.size(), CMB_SERIALIZER.STRING_SERIALIZER,
                        CMB_SERIALIZER.STRING_SERIALIZER);
                break;
            } else {
                //keep the last subscription for pagination purpose.
                nextTokenSub = subs.get(subs.size() - 1);
                nextToken = nextTokenSub.getArn();
                subs.remove(subs.size() - 1);
                deleteIndexesAll(subs);
                subs = listSubscriptionsByTopic(nextToken, topicArn, null, pageSize, false);
                deleteIndexes(nextTokenSub.getArn(), nextTokenSub.getUserId(), nextTokenSub.getToken());
                cassandraHandler.incrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                        topicArn, "subscriptionDeleted", subs.size(), CMB_SERIALIZER.STRING_SERIALIZER,
                        CMB_SERIALIZER.STRING_SERIALIZER);
            }
        }

        long subscriptionConfirmed = cassandraHandler.getCounter(AbstractDurablePersistence.CNS_KEYSPACE,
                columnFamilyTopicStats, topicArn, "subscriptionConfirmed", CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);

        if (subscriptionConfirmed > 0) {
            cassandraHandler.decrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                    topicArn, "subscriptionConfirmed", (int) subscriptionConfirmed,
                    CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
        }

        long subscriptionPending = cassandraHandler.getCounter(AbstractDurablePersistence.CNS_KEYSPACE,
                columnFamilyTopicStats, topicArn, "subscriptionPending", CMB_SERIALIZER.STRING_SERIALIZER,
                CMB_SERIALIZER.STRING_SERIALIZER);

        if (subscriptionPending > 0) {
            cassandraHandler.decrementCounter(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilyTopicStats,
                    topicArn, "subscriptionPending", (int) subscriptionPending, CMB_SERIALIZER.STRING_SERIALIZER,
                    CMB_SERIALIZER.STRING_SERIALIZER);
        }

        cassandraHandler.delete(AbstractDurablePersistence.CNS_KEYSPACE, columnFamilySubscriptions, topicArn, null,
                CMB_SERIALIZER.STRING_SERIALIZER, CMB_SERIALIZER.STRING_SERIALIZER);
    }

    @Override
    public void setRawMessageDelivery(String subscriptionArn, boolean rawMessageDelivery) throws Exception {
        CNSSubscription sub;
        sub = getSubscription(subscriptionArn);
        if (sub != null) {
            sub.setRawMessageDelivery(rawMessageDelivery);
            insertOrUpdateSubsAndIndexes(sub, null);
        }
    }
}