package org.ff4j.cassandra.store; /* * #%L * ff4j-store-cassandra * %% * Copyright (C) 2013 - 2016 FF4J * %% * 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. * #L% */ import static org.ff4j.cassandra.CassandraConstants.COLUMN_FAMILY_FEATURES; import static org.ff4j.cassandra.CassandraConstants.COL_FEAT_GROUPNAME; import static org.ff4j.cassandra.CassandraConstants.COL_FEAT_UID; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.ff4j.cassandra.CassandraConnection; import org.ff4j.cassandra.CassandraMapper; import org.ff4j.cassandra.CassandraQueryBuilder; import org.ff4j.core.Feature; import org.ff4j.core.FeatureStore; import org.ff4j.property.Property; import org.ff4j.store.AbstractFeatureStore; import org.ff4j.utils.JsonUtils; import org.ff4j.utils.Util; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; /** * Implementation of {@link FeatureStore} to work with Cassandra Storage. * * Minimize the Number of Writes : * Writes in Cassandra aren’t free, but they’re awfully cheap. Cassandra is optimized for high write throughput, * and almost all writes are equally efficient [1]. If you can perform extra writes to improve the efficiency of * your read queries, it’s almost always a good tradeoff. Reads tend to be more expensive and are much more * difficult to tune. * * Minimize Data Duplication * Denormalization and duplication of data is a fact of life with Cassandra. Don’t be afraid of it. Disk space * is generally the cheapest resource (compared to CPU, memory, disk IOPs, or network), and Cassandra is * architected around that fact. In order to get the most efficient reads, you often need to duplicate data. * * Rule 1: Spread Data Evenly Around the Cluster * Rule 2: Minimize the Number of Partitions Read * * Ces familles de colonnes contiennent des colonnes ainsi qu'un ensemble de colonnes connexes qui sont identifiées par une clé de ligne * * * @author Cedrick Lunven (@clunven) */ public class FeatureStoreCassandra extends AbstractFeatureStore { /** Connection to store Cassandra. */ private CassandraQueryBuilder builder; /** Connection to store Cassandra. */ private CassandraConnection conn; /** * Default constructor. */ public FeatureStoreCassandra() { } /** * Initialization through {@link CassandraConnection}. * * @param conn * current client to cassandra db */ public FeatureStoreCassandra(CassandraConnection conn) { this.conn = conn; } /** {@inheritDoc} */ @Override public void createSchema() { // Roles & custom properties will be in the same column family if (!conn.isColumnFamilyExist(COLUMN_FAMILY_FEATURES)) { // Create table conn.getSession().execute(getBuilder().cqlCreateColumnFamilyFeature()); // Add a secondary index to query conn.getSession().execute(getBuilder().cqlCreateIndexGroupName()); } } /** {@inheritDoc} */ @Override public boolean exist(String uid) { Util.assertHasLength(uid); return 1 == conn.getSession() .execute(getBuilder().cqlExistFeature(), uid) .iterator().next().getLong(0); } /** {@inheritDoc} */ @Override public void enable(String uid) { assertFeatureExist(uid); conn.getSession().execute(getBuilder().cqlEnableFeature(), uid); } /** {@inheritDoc} */ @Override public void disable(String uid) { assertFeatureExist(uid); conn.getSession().execute(getBuilder().cqlDisableFeature(), uid); } /** {@inheritDoc} */ @Override public void create(Feature fp) { assertFeatureNotNull(fp); assertFeatureNotExist(fp.getUid()); // Convert map<String, Property> to map<String, String>, structure in DB Map < String, String > mapOfProperties = new HashMap<String, String>(); if (fp.getCustomProperties() != null && !fp.getCustomProperties().isEmpty()) { for (Map.Entry<String, Property<?>> customP : fp.getCustomProperties().entrySet()) { if (customP.getValue() != null) { mapOfProperties.put(customP.getKey(), customP.getValue().toJson()); } } } conn.getSession().execute(getBuilder().cqlCreateFeature(), fp.getUid(), fp.isEnable() ? 1 : 0, fp.getDescription(), JsonUtils.flippingStrategyAsJson(fp.getFlippingStrategy()), fp.getGroup(), fp.getPermissions(), mapOfProperties); } /** {@inheritDoc} */ @Override public void delete(String uid) { assertFeatureExist(uid); conn.getSession().execute(getBuilder().cqlDeleteFeature(), uid); } /** {@inheritDoc} */ @Override public Feature read(String uid) { assertFeatureExist(uid); ResultSet rs = conn.getSession().execute(getBuilder().cqlReadFeature(), uid); return CassandraMapper.mapFeature(rs.one()); } /** {@inheritDoc} */ @Override public Map<String, Feature> readAll() { Map < String, Feature> features = new HashMap<String, Feature>(); ResultSet resultSet = conn.getSession().execute(getBuilder().selectAllFeatures()); for (Row row : resultSet.all()) { Feature f = CassandraMapper.mapFeature(row); features.put(f.getUid(), f); } return features; } /** {@inheritDoc} */ @Override public void update(Feature fp) { assertFeatureNotNull(fp); assertFeatureExist(fp.getUid()); // easiest way to perform delta update (lot of attributes) delete(fp.getUid()); create(fp); } /** {@inheritDoc} */ @Override public void grantRoleOnFeature(String uid, String roleName) { assertFeatureExist(uid); Util.assertHasLength(roleName); conn.getSession().execute(getBuilder().cqlGrantRoleOnFeature(roleName), uid); } /** {@inheritDoc} */ @Override public void removeRoleFromFeature(String uid, String roleName) { assertFeatureExist(uid); Util.assertHasLength(roleName); // Read role from target feature ResultSet rs = conn.getSession().execute(getBuilder().cqlReadFeatureRoles(), uid); Set <String> permissions = CassandraMapper.mapFeaturePermissions(rs.one()); // Remove expected permissions.remove(roleName); // Update new roleSet conn.getSession().execute(getBuilder().cqlUpdateFeatureRoles(), permissions, uid); } /** {@inheritDoc} */ @Override public void enableGroup(String groupName) { assertGroupExist(groupName); /* Even with secondary index the 'update SET enable =1 WHERE GROUPNAME=?' does not work * We will update each feature one by one */ ResultSet rs = conn.getSession().execute( getBuilder().cqlGetFeaturesNamesOfAGroup(), groupName); for (Row row : rs.all()) { enable(row.getString(COL_FEAT_UID)); } } /** {@inheritDoc} */ @Override public void disableGroup(String groupName) { assertGroupExist(groupName); ResultSet rs = conn.getSession().execute( getBuilder().cqlGetFeaturesNamesOfAGroup(), groupName); for (Row row : rs.all()) { disable(row.getString(COL_FEAT_UID)); } } /** {@inheritDoc} */ @Override public boolean existGroup(String groupName) { Util.assertHasLength(groupName); return 0 != conn.getSession() .execute(getBuilder().cqlExistGroup(), groupName) .iterator().next().getLong(0); } /** {@inheritDoc} */ @Override public Map<String, Feature> readGroup(String groupName) { assertGroupExist(groupName); Map<String, Feature> result = new HashMap<String, Feature>(); ResultSet rs = conn.getSession() .execute(getBuilder().cqlGetFeaturesOfAGroup(), groupName); for (Row row : rs.all()) { Feature f = CassandraMapper.mapFeature(row); result.put(f.getUid(), f); } return result; } /** {@inheritDoc} */ @Override public void addToGroup(String uid, String groupName) { assertFeatureExist(uid); Util.assertHasLength(groupName); conn.getSession().execute(getBuilder().cqlAddFeatureToGroup(), groupName, uid); } /** {@inheritDoc} */ @Override public void removeFromGroup(String uid, String groupName) { assertFeatureExist(uid); assertGroupExist(groupName); conn.getSession().execute(getBuilder().cqlRemoveFeatureFromGroup(), uid); } /** {@inheritDoc} */ @Override public Set<String> readAllGroups() { ResultSet rs = conn.getSession().execute(getBuilder().cqlGetGroups()); Set< String > groups = new HashSet<String>(); for (Row row : rs.all()) { groups.add(row.getString(COL_FEAT_GROUPNAME)); } groups.remove(null); groups.remove(""); return groups; } /** {@inheritDoc} */ @Override public void clear() { conn.getSession().execute(getBuilder().cqlTruncateFeatures()); } /** * Getter accessor for attribute 'conn'. * * @return * current value of 'conn' */ public CassandraConnection getConn() { return conn; } /** * Setter accessor for attribute 'conn'. * @param conn * new value for 'conn ' */ public void setConn(CassandraConnection conn) { this.conn = conn; } /** * Getter accessor for attribute 'builder'. * * @return * current value of 'builder' */ public CassandraQueryBuilder getBuilder() { if (builder == null) { builder = new CassandraQueryBuilder(conn); } return builder; } }