/* * 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.wizecommerce.hecuba; import java.nio.ByteBuffer; import java.util.*; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.wizecommerce.hecuba.util.ConfigUtils; /** * This is a convenience class that will enable a user to interact with Column Families. It is * assumed that all the column names are strings. * * This is a stateful implementation of the Cassandra manager. The parameters set either in the constructor and/or * setter methods will be used to carry our update/read/delete operations. * * * @author - Eran Chinthaka Withana */ public abstract class HecubaClientManager<K> { protected String clusterName; protected String locationURLs; protected String ports; protected String keyspace; protected String columnFamily; protected int maxColumnCount; // used through property if column count in row > 100 protected int maxSiColumnCount; protected String username; protected String password; // being a schema flexible datastore we sometimes need to create secondary indexes based on the names of the // columns. The following parameter will enable the secondary index columns. If you set this parameter, // we will match each and every column name with the given pattern and create a secondary index out of this. protected String secondaryIdxByColumnPattern = null; protected boolean isClientAdapterDebugMessagesEnabled; /** * Secondary Index Implementation ============================== * * We tried to use Cassandra's in-built secondary indexes but it ended up in tears as these hidden secondary * indexes * sometimes didn't return proper results. Even more than that some of the results were intermittent, meaning the * same request works and not works sometimes. * * Due to this complication, we decided to maintain our own secondary indexes as of 14th November, 2012. * * In this implementation, each object has its own column family to store the secondary indexes. During a writing, * we intercept those calls and check whether its a change to the secondary index. If it is, then we will delete * the * previous entry, if any, and create a new entry in the table mapping the new secondary index to the original * object. * * In the read path, we will provide a new method, retrieveBySecondaryIndex(String columnName, String * value):List<String>, where the list contains the set of ids linking to the main table. * * Features: + we will be able to have indexes for any number of columns + the implementation of secondary indexes * will be hidden from the client and he/she has to do minimal amount of work to get it working */ protected List<String> columnsToIndexOnColumnNameAndValue; protected boolean isSecondaryIndexByColumnNameAndValueEnabled = false; protected boolean isSecondaryIndexesByColumnNamesEnabled = false; protected static Logger log = Logger.getLogger(HecubaClientManager.class); /** * Creates an instance of the Hecuba client manager to make the calls to Cassandra cluster easier. * * @param parameters -- */ public HecubaClientManager(CassandraParamsBean parameters) { this.clusterName = parameters.getClustername(); this.locationURLs = parameters.getLocationURLs(); this.ports = parameters.getThriftPorts(); this.keyspace = parameters.getKeyspace(); this.columnFamily = parameters.getColumnFamily(); this.maxColumnCount = parameters.getMaxColumnCount(); this.maxSiColumnCount = parameters.getMaxSiColumnCount(); this.username = parameters.getUsername(); this.password = parameters.getPassword(); if (StringUtils.isNotBlank(parameters.getSiColumns())) { columnsToIndexOnColumnNameAndValue = Arrays.asList(StringUtils.split(parameters.getSiColumns(), ":")); Collections.sort(columnsToIndexOnColumnNameAndValue); isSecondaryIndexByColumnNameAndValueEnabled = true; } String siByColumnsPattern = parameters.getSiByColumnsPattern(); if (StringUtils.isNotEmpty(siByColumnsPattern)) { this.secondaryIdxByColumnPattern = siByColumnsPattern; this.isSecondaryIndexesByColumnNamesEnabled = true; } init(); } /** * WARNING: Use the following constructor for testing purpose only. Please use {@link * HecubaClientManager#HecubaClientManager(CassandraParamsBean)} for other purposes Creates an instance of * the * manager to make the calls to Cassandra cluster easier. The parameters provided in this constructor can be * changed * later, if needed, using the relevant setter methods. Note that frequent changes to these parameters can degrade * the performance. * * @param clusterName - name of the cassandra cluster * @param locationURL - pointer to a node in the cassandra cluster * @param ports - the port at which the node you are connecting is listening to thrift messages. Default is * 9106 and this value is printed on the console when you start Cassandra instance. * @param keyspace - the keyspace you will be managing using this manager. * @param columnFamily - the column family you will be managing using this manager. */ public HecubaClientManager(String clusterName, String locationURL, String ports, String keyspace, String columnFamily) { super(); this.clusterName = clusterName; this.locationURLs = locationURL; this.ports = ports; this.keyspace = keyspace; this.columnFamily = columnFamily; init(); } /** * Write stuff in init() method which is common to all constructors of this class */ private void init() { this.isClientAdapterDebugMessagesEnabled = ConfigUtils.getInstance().getConfiguration().getBoolean( HecubaConstants.ENABLE_DEBUG_MESSAGES, false); } /** * WARNING: Following constructor is for testing purposes only. Please use {@link * HecubaClientManager#HecubaClientManager()} (String, String, String, String, String, * me.prettyprint.hector.api.Serializer)} or * {@link HecubaClientManager#HecubaClientManager(CassandraParamsBean)} for other purposes. */ public HecubaClientManager() { } public void setColumnsToIndexOnColumnNameAndValue(List<String> columnNames) { this.columnsToIndexOnColumnNameAndValue = columnNames; } /** * Updates the value of the column in the given row (identified by the key). * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public void updateString(K key, String columnName, String value) { updateString(key, columnName, value, -1, -1); } /** * This method will give the user more power to control what goes into Cassandra. With every column insert, one can * specify four different parameters. * * <ol> <li>Column name</li> <li>Column value</li> <li>Timestamp - this, if not specified, is set by our client * library. </li> <li>Time To Live (TTL) - this, if not specified, is set to be indefinite.</li> </ol> * * This method allows one to specify all the above parameters, but @link updateString(K key, String columnName, * String value) will set the defaults to ttl and timestamps. * * Also, if you want to set only parameters and not care about the other, feel free to set the value to -1 or any * negative value so that we will detect it and set the default values. For example, you can invoke this method * with updateString(1234, "MyColumn", "MyValue", -1, 22211). This will only set the given ttl but use the client * library to set the timestamp. * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. * @param timestamp - timestamp to set. Make sure this is in nanoseconds * @param ttl - time-to-live for the column so that Cassandra could expire it */ public abstract void updateString(K key, String columnName, String value, long timestamp, int ttl); /** * Updates the value of the column in the given row (identified by the key). * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public abstract void updateByteBuffer(K key, String columnName, ByteBuffer value); /** * Updates the value of the column in the given row (identified by the key). * * Note that the value will be converted to a String before storing in Cassandra. * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public void updateBoolean(K key, String columnName, Boolean value) { updateString(key, columnName, value ? "true" : "false"); } /** * Updates the value of the column in the given row (identified by the key). * * Note that the value will be converted to a String before storing in Cassandra. * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public void updateDate(K key, String columnName, Date value) { updateString(key, columnName, HecubaConstants.DATE_FORMATTER.print(value.getTime())); } /** * Updates the value of the column in the given row (identified by the key). * * Note that the value will be converted to a String before storing in Cassandra. * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public void updateDouble(K key, String columnName, Double value) { updateString(key, columnName, value.toString()); } /** * Updates the value of the column in the given row (identified by the key). * * Note that the value will be converted to a String before storing in Cassandra. * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public void updateLong(K key, String columnName, Long value) { updateString(key, columnName, value.toString()); } /** * Updates the value of the column in the given row (identified by the key). * * Note that the value will be converted to a String before storing in Cassandra. * * @param key - key to identify the row. * @param columnName - column to be updated. * @param value - value to be inserted. */ public void updateInteger(K key, String columnName, Integer value) { updateString(key, columnName, value.toString()); } // ==================================================================================== /** * Updates a complete row and uses the same timestamp. * * Note: We are pushing even the columns with null values by putting "null" string to those corresponding columns. * This is useful because if a column currently has a non-null value and if one wants to remove it he should be * able * to set the null value. * * @param key - key of the column to be updated. * @param row - a map of columnNames and their respected values to be updated. * * @throws Exception Error occurred during read */ public void updateRow(K key, Map<String, Object> row) throws Exception { updateRow(key, row, null, null); } /** * Updates a complete row and uses the given timestamps and ttl values for each column. * * If you want to use the default values for any of the columns, either do not add that column name mapping to * timetamps or ttls maps or set the value to a negative value. Setting any of these columns to null will make the * defaults to be applied for all the columns. * * @param key - key of the row to be updated. * @param row - a map of column names and their values * @param timestamps - a map of column names to their timestamps. Defaults to the underlying clients timestamp * implementation. * @param ttls - a map of column names to their ttls. Defaults to not expire. * * @throws Exception Error occurred during update */ public abstract void updateRow(K key, Map<String, Object> row, Map<String, Long> timestamps, Map<String, Integer> ttls) throws Exception; /** * Reads the value of a column related to a given key. * * @param key - key of the column to be read. * @param columnName - name of the column to be read. * * @return the value for the given key and column */ public abstract String readString(K key, String columnName); /** * Retrieves a column together with its name, value, timestamp and ttl. * * @param key - key of the column to be read. * @param columnName - name of the column to be read. * * @return CassandraColumn containing the name, value, timestamp and ttl for the given key and column */ public abstract CassandraColumn readColumnInfo(K key, String columnName); /** * Reads the value of a column related to a given key and returns it as a boolean. * * Note: since all the values are stored as Strings this method will explicitly convert the string to a boolean. * * @param key - key of the column to be read. * @param columnName - name of the column to be read. * * @return the value for the given key and column */ public Boolean readBoolean(K key, String columnName) { return readBoolean(key, columnName, false); } /** * Reads the value of a column related to a given key and returns it as a boolean. * * Note: since all the values are stored as Strings this method will explicitly convert the string to a boolean. * * @param key - key of the column to be read. * @param columnName - name of the column to be read. * @param defaultValue - default value if value is not found in cassandra * * @return the value for the given key and column */ public Boolean readBoolean(K key, String columnName, boolean defaultValue) { final String value = readString(key, columnName); return value == null ? defaultValue : ("true".equalsIgnoreCase(value) || "1".equals(value)); } public Date readDate(K key, String columnName) { return readDate(key, columnName, null); } public Date readDate(K key, String columnName, Date defaultDate) { final String value = readString(key, columnName); return value == null ? defaultDate : HecubaConstants.DATE_FORMATTER.parseDateTime(value).toDate(); } public Integer readInteger(K key, String columnName) { return readInteger(key, columnName, null); } public Integer readInteger(K key, String columnName, Integer defaultInt) { final String value = readString(key, columnName); return value == null ? defaultInt : Integer.parseInt(value); } public Long readLong(K key, String columnName) { return readLong(key, columnName, -1); } public Long readLong(K key, String columnName, long defaultLong) { final String value = readString(key, columnName); return value == null ? defaultLong : Long.parseLong(value); } public Double readDouble(K key, String columnName) { return readDouble(key, columnName, -1.0); } public Double readDouble(K key, String columnName, double defaultDouble) { final String value = readString(key, columnName); return value == null ? defaultDouble : Double.parseDouble(value); } /** * Retrieves all the columns related to a given key. * Analogous to "Select * from TABLE" in SQL world. * * If column family can contain more than 100 columns, set the maxColumnCount * to the max no of columns possible in the column family row. * Otherwise only 100 columns would be fetched. * * @param key - key of the column to be read. * * @return CassandraResultSet (interface to get column values) * @throws Exception Error occurred during read */ public abstract CassandraResultSet<K, String> readAllColumns(K key) throws Exception; /** * Retrieves set of columns (within specified range) for the key * * @param key - key of the column family row * @param start - column name marking the start of range (null for no boundary on start) * @param end - column name marking the end of range (null for no boundary on end) * @param reversed - whether the results should be ordered in reversed order (Similar to ORDER BY blah DESC in * SQL). * When reversed is true, start will determine the right end of the range while finish will * determine the left, * meaning start must be >= finish. * @param count - number of columns to return (Analogous to "limit X" in SQL world). * * @return CassandraResultSet (interface to get column values) * If key doesn't exist in Cassandra, returned ResultSet is empty (i.e. no columns present in the result * set) */ public abstract CassandraResultSet<K, String> readColumnSlice(K key, String start, String end, boolean reversed, int count); /** * Retrieves all columns (upto a limit of 10000 columns) for the key. * * If number of columns is very large (say 1M), fetching all of them can cause memory issues * and for that pagination should be used. * * @param key - key of the column family row * * @return CassandraResultSet (interface to iterate the columns to get column values) * If key doesn't exist in Cassandra, returned ResultSet is empty (i.e. no columns present in the result * set) */ public CassandraResultSet<K, String> readColumnSliceAllColumns(K key) { return readColumnSlice(key, null, null, false, 10000); } /** * Get the value of a counter and returns 0 if the counter is not available. * * @param key - key of the column to be read. * @param counterColumnName - name of the column to be read. * * @return the value for the given key and column */ public abstract Long getCounterValue(K key, String counterColumnName); /** * Update the counter with the given value (which can be either negative or positive). * If the counter does not exist, a new counter will be created with the given value. * * @param key - key of the column to be read. * @param counterColumnName - name of the column to be read. * @param value - amount to increment counter by * */ public abstract void updateCounter(K key, String counterColumnName, long value); /** * Increase the counter by 1. * If the counter does not exist, a counter with the default value 0 will be created and then incremented. * * @param key - key of the column to be read. * @param counterColumnName - name of the column to be read. */ public abstract void incrementCounter(K key, String counterColumnName); /** * Decrease the counter by 1. * If the counter does not exist, a counter with the default value 0 will be created and then decremented. * * @param key - key of the column to be read. * @param counterColumnName - name of the column to be read. */ public abstract void decrementCounter(K key, String counterColumnName); /** * Retrieves all the columns for the list of keys. In the implementation, check for Id (eg. TAG_ID) column to * determine if the record exists (and put the object in the map). * If column family can contain more than 100 columns, set the maxColumnCount * to the max no of columns possible in the column family row. * Otherwise only 100 columns would be fetched. * * @param keys - Set of keys to retrieve * * @return CassandraResultSet<K, String>. CassandraResultSet wraps the object returned by Hector/Astyanax API. * * @throws Exception Error occurred during read */ public abstract CassandraResultSet<K, String> readAllColumns(Set<K> keys) throws Exception; /** * Retrieves set of columns (within specified range) for the each key * * @param keys - set of keys for which columns need to be retrieved * @param start - column name marking the start of range (null for no boundary on start) * @param end - column name marking the end of range (null for no boundary on end) * @param reversed - whether the results should be ordered in reversed order (Similar to ORDER BY blah DESC in * SQL). * When reversed is true, start will determine the right end of the range while finish will * determine the left, * meaning start must be >= finish. * * @return CassandraResultSet (interface to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). */ public abstract CassandraResultSet<K, String> readColumnSlice(Set<K> keys, String start, String end, boolean reversed); /** * Retrieves all columns (upto a limit of 10000 columns) for set of keys. * If number of columns is very large (say 1M), fetching all of them can cause memory issues * and for that pagination should be used. * * @param keys - set of keys for which all the columns need to be retrieved * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). */ public CassandraResultSet<K, String> readColumnSliceAllColumns(Set<K> keys) { return readColumnSlice(keys, null, null, false); } /** * Retrieves only the set of column values. * This can be helpful where the user needs only few columns instead of reading all columns (say 100) stored in a * row * * @param key - key of the column family row * @param columnNames - List of column names * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). * * @throws Exception Error occurred during read */ public abstract CassandraResultSet<K, String> readColumns(K key, List<String> columnNames) throws Exception; /** * Retrieves only the set of column values for given keys. * This can be helpful where the user needs only few columns instead of reading all columns in given rows. * * @param keys - Set of keys for which specified columns need to be retrieved * @param columnNames - List of column names * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). * * @throws Exception Error occurred during read */ public abstract CassandraResultSet<K, String> readColumns(Set<K> keys, List<String> columnNames) throws Exception; /** * Deletes a given column value of a row identified by the key. * * @param key - key of the row. * @param columnName - name of the column to be deleted. */ public abstract void deleteColumn(K key, String columnName); /** * Deletes a given column value of a row identified by the key. * * @param key - key of the row. * @param columnNameList - a list that contains name of the columns to be deleted. */ public abstract void deleteColumns(K key, List<String> columnNameList); /** * Deletes an entire row for a given key. * * @param key - the key of the row to be deleted. */ public void deleteRow(K key) { deleteRow(key, -1); } public abstract void deleteRow(K key, long timestamp); // ==================================================== // Secondary Index Related Methods // ==================================================== /** * We are indexing based on the name of the column AND the value stored under that column. For example, * if we have a person column family, keyed by person id/name, we might also want to retrieve the people by the * department they belong to. In that case, we should be creating a secondary index on the name and the value of * department column. * * The row must match all parameters passed in. For example if parameters = {a: x, b: y} then the row must have column a set to x AND column b set to y to be returned. * * @param parameters a map of all values the row must match. * @param limit the maximum number of columns to retrieve from matching rows * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). */ public abstract CassandraResultSet readAllColumnsBySecondaryIndex(Map<String, String> parameters, int limit); /** * We are indexing based on the name of the column AND the value stored under that column. For example, * if we have a person column family, keyed by person id/name, we might also want to retrieve the people by the * department they belong to. In that case, we should be creating a secondary index on the name and the value of * department column. * * @param columnName : name of the column to retrieve * @param columnValue : value of the column * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). */ public abstract CassandraResultSet<K, String> retrieveBySecondaryIndex(String columnName, String columnValue); /** * We are indexing based on the name of the column AND the value stored under that column. For example, * if we have a person column family, keyed by person id/name, we might also want to retrieve the people by the * department they belong to. In that case, we should be creating a secondary index on the name and the value of * department column. * * This method will allow to do a multi get based on the secondary index. * * @param columnName : name of the column to retrieve * @param columnValue : value of the column * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). */ public abstract CassandraResultSet<K, String> retrieveBySecondaryIndex(String columnName, List<String> columnValue); /** * There are scenarios where we need to create secondary indexes only on the name of the column name. This * especially happens if we encode some information in the column name itself. For example, * we could set the name of the column to be the stocks a person had purchased whereas the value corresponds to * the specific details of stock purchase. At one point of time if someone wants to find out the people * associated with a stock, we can easily do that by creating a secondary index on the stock. * * @param columnName : name of the column to be retrieved * * @return CassandraResultSet (interface to iterate the columns to get column values) * If any key(s) doesn't exist in Cassandra, returned ResultSet will contain no column for those key(s). */ public abstract CassandraResultSet<K, String> retrieveByColumnNameBasedSecondaryIndex(String columnName); /** * We are indexing based on the name of the column AND the value stored under that column. * Sometimes there are use cases to only retrieve keys (and not all columns) using secondary index. * Furthermore, after retrieving keys, client can retrieve only specific columns using {@link HecubaClientManager#readColumns(Object, List)} * * {@link HecubaClientManager#retrieveBySecondaryIndex(String, String)} returns {@link CassandraResultSet} by reading allColumns * * @param columnName - Cassandra column name which is secondary indexed * @param columnValue - Cassandra column value which is secondary indexed * @return ordered list of keys */ public abstract List<K> retrieveKeysBySecondaryIndex(String columnName, String columnValue); /** * We are indexing based on the name of the column AND the value stored under that column. * Retrieve keys using secondary index for multiple column values. * This is a multi-get for {@link HecubaClientManager#retrieveKeysBySecondaryIndex(String, String)} * * {@link HecubaClientManager#retrieveBySecondaryIndex(String, List)} returns result by reading allColumns * @param columnName - Cassandra column name which is secondary indexed * @param columnValues - list of cassandra column values which are secondary indexed * @return map of columnValue to list of keys for that column value */ public abstract Map<String, List<K>> retrieveKeysBySecondaryIndex(String columnName, List<String> columnValues); /** * There are scenarios where we need to create secondary indexes only on the name of the column name. * Sometimes there are use cases to only retrieve keys (and not all columns) using secondary index. * Furthermore, after retrieving keys, client can retrieve only specific columns using {@link HecubaClientManager#readColumns(Object, List)} * * {@link HecubaClientManager#retrieveByColumnNameBasedSecondaryIndex(String)} returns result by reading allColumns * @param columnName - Cassandra column name which is secondary indexed * @return list of keys that have the supplied column in their row */ public abstract List<K> retrieveKeysByColumnNameBasedSecondaryIndex(String columnName); // ==================================================== // Utils // ==================================================== /** * Takes a list of comma delimited urls, and a list of comma delimited port numbers. * It will match the pairs of port to hostnames. If the counts don't match, then it will * backfill with the last valid port number. * @param locationURLs address of cassandra nodes * @param ports ports that cassandra nodes are listening on * @return locationURLs combined with ports (location1:port1,location2:port2,...) */ protected String getListOfNodesAndPorts(String locationURLs, String ports) { final String paramSeparator = ConfigUtils.getInstance().getConfiguration().getString( HecubaConstants.GLOBAL_PROP_NAME_PREFIX + ".hecuba.path.separator", ":"); final String[] splittedPorts = ports.split(paramSeparator); final String[] splittedLocationURLs = locationURLs.split(paramSeparator); final StringBuffer listOfNodesAndPortsBuffer = new StringBuffer(); String port = ""; for (int index = 0; index < splittedLocationURLs.length; index++) { final String locationURL = splittedLocationURLs[index]; if (index < splittedPorts.length) { port = splittedPorts[index]; } if (StringUtils.isEmpty(port)) { port = "9160"; } listOfNodesAndPortsBuffer.append(locationURL).append(":").append(port).append(","); } return listOfNodesAndPortsBuffer.substring(0, listOfNodesAndPortsBuffer.lastIndexOf(",")); } // ==================================================== // Getters and Setters // ==================================================== /** * @return the clusterName */ public String getClusterName() { return clusterName; } /** * @param clusterName the clusterName to set */ public void setClusterName(String clusterName) { this.clusterName = clusterName; } /** * @return the locationURL */ public String getLocationURL() { return locationURLs; } /** * @param locationURL the locationURL to set */ public void setLocationURL(String locationURL) { this.locationURLs = locationURL; } /** * @return the port */ public String getPort() { return ports; } /** * @return the keyspace */ public String getKeyspace() { return keyspace; } /** * @return the columnFamily */ public String getColumnFamilyName() { return columnFamily; } /** * @param columnFamily the columnFamily to set */ public void setColumnFamily(String columnFamily) { this.columnFamily = columnFamily; } public List<String> getColumnsToIndexOnColumnNameAndValue() { return columnsToIndexOnColumnNameAndValue; } public String getSecondaryIndexKey(String columnName, String columnValue) { if (columnValue == null || "null".equals(columnValue)) { columnValue = ""; } return columnName + ":" + columnValue; } /** * Gracefully shuts down the cluster closing all cassandra connections. */ public abstract void shutDown(); protected String getSecondaryIndexedColumnValue(String secondaryIndexKey) { if (secondaryIndexKey.length() > secondaryIndexKey.indexOf(":") + 1) { return secondaryIndexKey.substring(secondaryIndexKey.indexOf(":") + 1, secondaryIndexKey.length()); } else { return ""; } } protected List<String> getSecondaryIndexKeys(String columnName, List<String> columnValues) { List<String> secondaryIndexKeys = null; if (columnValues != null && columnValues.size() > 0) { secondaryIndexKeys = new ArrayList<>(); for (String columnValue : columnValues) { secondaryIndexKeys.add(getSecondaryIndexKey(columnName, columnValue)); } } return secondaryIndexKeys; } protected boolean isSecondaryIndexByColumnNameEnabledForColumn(String columnName) { return isSecondaryIndexesByColumnNamesEnabled && StringUtils.isNotEmpty(columnName) && columnName.matches( secondaryIdxByColumnPattern); } protected boolean isSecondaryIndexByColumnNameAndValueEnabledForColumn(String columnName) { return isSecondaryIndexByColumnNameAndValueEnabled && Collections.binarySearch( columnsToIndexOnColumnNameAndValue, columnName) >= 0; } protected abstract void logDownedHosts(); }