/**
* Copyright (c) 2011-2012 Optimax Software Ltd.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Optimax Software, ElasticInbox, nor the names
* of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.elasticinbox.core.cassandra.persistence;
import static me.prettyprint.hector.api.factory.HFactory.createCounterColumn;
import static me.prettyprint.hector.api.factory.HFactory.createCounterSliceQuery;
import static com.elasticinbox.core.cassandra.CassandraDAOFactory.CF_COUNTERS;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.prettyprint.cassandra.serializers.CompositeSerializer;
import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.hector.api.beans.Composite;
import me.prettyprint.hector.api.beans.CounterSlice;
import me.prettyprint.hector.api.beans.HCounterColumn;
import me.prettyprint.hector.api.mutation.Mutator;
import me.prettyprint.hector.api.query.QueryResult;
import me.prettyprint.hector.api.query.SliceCounterQuery;
import com.elasticinbox.core.cassandra.CassandraDAOFactory;
import com.elasticinbox.core.model.LabelConstants;
import com.elasticinbox.core.model.LabelCounters;
import com.elasticinbox.core.model.ReservedLabels;
public final class LabelCounterPersistence
{
/** Counter type for Label counters */
public final static String CN_TYPE_LABEL = "l";
/** Label counter subtype for total bytes */
public final static char CN_SUBTYPE_BYTES = 'b';
/** Label counter subtype for total messages */
public final static char CN_SUBTYPE_MESSAGES = 'm';
/** Label counter subtype for unread messages */
public final static char CN_SUBTYPE_UNREAD = 'u';
private final static StringSerializer strSe = StringSerializer.get();
private final static Logger logger =
LoggerFactory.getLogger(LabelCounterPersistence.class);
/**
* Get counters for all label in the given mailbox
*
* @param mailbox
* @return
*/
public static Map<Integer, LabelCounters> getAll(final String mailbox)
{
Composite startRange = new Composite();
startRange.addComponent(0, CN_TYPE_LABEL, Composite.ComponentEquality.EQUAL);
Composite endRange = new Composite();
endRange.addComponent(0, CN_TYPE_LABEL, Composite.ComponentEquality.GREATER_THAN_EQUAL);
SliceCounterQuery<String, Composite> sliceQuery =
createCounterSliceQuery(CassandraDAOFactory.getKeyspace(), strSe, new CompositeSerializer());
sliceQuery.setColumnFamily(CF_COUNTERS);
sliceQuery.setKey(mailbox);
sliceQuery.setRange(startRange, endRange, false, LabelConstants.MAX_LABEL_ID);
QueryResult<CounterSlice<Composite>> r = sliceQuery.execute();
return compositeColumnsToCounters(mailbox, r.get().getColumns());
}
/**
* Get counters for the specified label in the given mailbox
*
* @param mailbox
* @param labelId
* @return
*/
public static LabelCounters get(final String mailbox, final Integer labelId)
{
Composite startRange = new Composite();
startRange.addComponent(0, CN_TYPE_LABEL, Composite.ComponentEquality.EQUAL);
startRange.addComponent(1, labelId.toString(), Composite.ComponentEquality.EQUAL);
Composite endRange = new Composite();
endRange.addComponent(0, CN_TYPE_LABEL, Composite.ComponentEquality.EQUAL);
endRange.addComponent(1, labelId.toString(), Composite.ComponentEquality.GREATER_THAN_EQUAL);
SliceCounterQuery<String, Composite> sliceQuery =
createCounterSliceQuery(CassandraDAOFactory.getKeyspace(), strSe, new CompositeSerializer());
sliceQuery.setColumnFamily(CF_COUNTERS);
sliceQuery.setKey(mailbox);
sliceQuery.setRange(startRange, endRange, false, 5);
QueryResult<CounterSlice<Composite>> r = sliceQuery.execute();
Map<Integer, LabelCounters> counters = compositeColumnsToCounters(mailbox, r.get().getColumns());
LabelCounters labelCounters = counters.containsKey(labelId) ? counters.get(labelId) : new LabelCounters();
logger.debug("Fetched counters for single label {} with {}", labelId, labelCounters);
return labelCounters;
}
/**
* Increment or decrement of the label counters. Use negative values for
* decrement.
*
* @param mutator
* @param mailbox
* @param labelIds
* @param labelCounters
*/
public static void add(Mutator<String> mutator, final String mailbox,
final Set<Integer> labelIds, final LabelCounters labelCounters)
{
// batch add of counters for each of the labels
for (Integer labelId : labelIds)
{
logger.debug("Updating counters for label {} with {}", labelId, labelCounters);
// update total bytes only for ALL_MAILS label (i.e. total mailbox usage)
if ((labelId == ReservedLabels.ALL_MAILS.getId()) && (labelCounters.getTotalBytes() != 0)) {
HCounterColumn<Composite> col = countersToCompositeColumn(
labelId, CN_SUBTYPE_BYTES, labelCounters.getTotalBytes());
mutator.addCounter(mailbox, CF_COUNTERS, col);
}
if (labelCounters.getTotalMessages() != 0) {
HCounterColumn<Composite> col = countersToCompositeColumn(
labelId, CN_SUBTYPE_MESSAGES, labelCounters.getTotalMessages());
mutator.addCounter(mailbox, CF_COUNTERS, col);
}
if (labelCounters.getUnreadMessages() != 0) {
HCounterColumn<Composite> col = countersToCompositeColumn(
labelId, CN_SUBTYPE_UNREAD, labelCounters.getUnreadMessages());
mutator.addCounter(mailbox, CF_COUNTERS, col);
}
}
}
public static void subtract(Mutator<String> mutator, final String mailbox,
final Set<Integer> labelIds, final LabelCounters labelCounters)
{
// perform addition of inverse (i.e. subtraction)
add(mutator, mailbox, labelIds, labelCounters.getInverse());
}
public static void add(Mutator<String> mutator, final String mailbox,
final Integer labelId, final LabelCounters labelCounters)
{
Set<Integer> labelIds = new HashSet<Integer>(1);
labelIds.add(labelId);
add(mutator, mailbox, labelIds, labelCounters);
}
public static void subtract(Mutator<String> mutator, final String mailbox,
final Integer labelId, final LabelCounters labelCounters)
{
Set<Integer> labelIds = new HashSet<Integer>(1);
labelIds.add(labelId);
subtract(mutator, mailbox, labelIds, labelCounters);
}
/**
* Delete label counters
*
* @param mutator
* @param mailbox
* @param labelId
*/
public static void delete(Mutator<String> mutator, final String mailbox,
final Integer labelId)
{
// reset all counters (since delete won't work in most cases)
// see: http://cassandra-user-incubator-apache-org.3065146.n2.nabble.com/possible-coming-back-to-life-bug-with-counters-tp6464338p6475427.html
LabelCounters labelCounters = get(mailbox, labelId);
// if counter super-column for this label exists
if (labelCounters != null) {
subtract(mutator, mailbox, labelId, labelCounters);
// delete counters
HCounterColumn<Composite> c;
c = countersToCompositeColumn(labelId, CN_SUBTYPE_MESSAGES, labelCounters.getTotalMessages());
mutator.addDeletion(mailbox, CF_COUNTERS, c.getName(), new CompositeSerializer());
c = countersToCompositeColumn(labelId, CN_SUBTYPE_UNREAD, labelCounters.getTotalMessages());
mutator.addDeletion(mailbox, CF_COUNTERS, c.getName(), new CompositeSerializer());
// delete bytes only if ALL_MAILS
if (labelId == ReservedLabels.ALL_MAILS.getId()) {
c = countersToCompositeColumn(labelId, CN_SUBTYPE_BYTES, labelCounters.getTotalMessages());
mutator.addDeletion(mailbox, CF_COUNTERS, c.getName(), new CompositeSerializer());
}
}
}
/**
* Delete all label counters
*
* @param mailbox
*/
public static void deleteAll(Mutator<String> mutator, final String mailbox)
{
// reset all counters (since delete won't work in most cases)
// see: http://cassandra-user-incubator-apache-org.3065146.n2.nabble.com/possible-coming-back-to-life-bug-with-counters-tp6464338p6475427.html
Map<Integer, LabelCounters> counters = getAll(mailbox);
for (Integer labelId : counters.keySet()) {
LabelCounters labelCounters = counters.get(labelId);
subtract(mutator, mailbox, labelId, labelCounters);
}
// delete all label counters
mutator.delete(mailbox, CF_COUNTERS, null, strSe);
}
/**
* Build composite counter column
*
* @param labelId
* @param subtype
* @param count
* @return
*/
private static HCounterColumn<Composite> countersToCompositeColumn(
final Integer labelId, final char subtype, final Long count)
{
Composite composite = new Composite();
composite.addComponent(CN_TYPE_LABEL, strSe);
composite.addComponent(labelId.toString(), strSe);
composite.addComponent(Character.toString(subtype), strSe);
return createCounterColumn(composite, count, new CompositeSerializer());
}
/**
* Convert Hector Composite Columns to {@link LabelCounters}
*
* @param columnList
* @return
*/
private static Map<Integer, LabelCounters> compositeColumnsToCounters(
final String mailbox, final List<HCounterColumn<Composite>> columnList)
{
Map<Integer, LabelCounters> result =
new HashMap<Integer, LabelCounters>(LabelConstants.MAX_RESERVED_LABEL_ID);
LabelCounters labelCounters = new LabelCounters();
int prevLabelId = 0; // remember previous labelid which is always start form 0
for (HCounterColumn<Composite> c : columnList)
{
int labelId = Integer.parseInt(c.getName().get(1, strSe));
char subtype = c.getName().get(2, strSe).charAt(0);
// since columns are ordered by labels, we can
// flush label counters to result map as we traverse
if (prevLabelId != labelId) {
logger.debug("Fetched counters for label {} with {}", prevLabelId, labelCounters);
result.put(prevLabelId, labelCounters);
labelCounters = new LabelCounters();
prevLabelId = labelId;
}
switch (subtype) {
case CN_SUBTYPE_BYTES:
labelCounters.setTotalBytes(c.getValue());
break;
case CN_SUBTYPE_MESSAGES:
labelCounters.setTotalMessages(c.getValue());
break;
case CN_SUBTYPE_UNREAD:
labelCounters.setUnreadMessages(c.getValue());
break;
}
if (c.getValue() < 0) {
logger.warn("Negative counter value found for label {}/{}: ", mailbox, labelId);
}
}
// flush remaining counters for the last label
result.put(prevLabelId, labelCounters);
return result;
}
}