package org.ff4j.store;
import static org.ff4j.store.JdbcStoreConstants.COL_FEAT_GROUPNAME;
import static org.ff4j.store.JdbcStoreConstants.COL_ROLE_FEATID;
import static org.ff4j.store.JdbcStoreConstants.COL_ROLE_ROLENAME;
import static org.ff4j.utils.JdbcUtils.buildStatement;
/*
* #%L
* ff4j-core
* %%
* Copyright (C) 2013 - 2015 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.utils.JdbcUtils.closeConnection;
import static org.ff4j.utils.JdbcUtils.closeResultSet;
import static org.ff4j.utils.JdbcUtils.closeStatement;
import static org.ff4j.utils.JdbcUtils.executeUpdate;
import static org.ff4j.utils.JdbcUtils.isTableExist;
import static org.ff4j.utils.JdbcUtils.rollback;
import static org.ff4j.utils.Util.assertHasLength;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.sql.DataSource;
import org.ff4j.core.Feature;
import org.ff4j.core.FeatureStore;
import org.ff4j.exception.FeatureAccessException;
import org.ff4j.exception.FeatureAlreadyExistException;
import org.ff4j.property.Property;
import org.ff4j.property.store.JdbcPropertyMapper;
import org.ff4j.utils.JdbcUtils;
import org.ff4j.utils.MappingUtil;
import org.ff4j.utils.Util;
/**
* Implementation of {@link FeatureStore} to work with RDBMS through JDBC.
*
* @author Cedrick Lunven (@clunven)
*/
public class JdbcFeatureStore extends AbstractFeatureStore {
/** Error message 1. */
public static final String CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE =
"Cannot check feature existence, error related to database";
/** Error message 2. */
public static final String CANNOT_UPDATE_FEATURES_DATABASE_SQL_ERROR =
"Cannot update features database, SQL ERROR";
/** Access to storage. */
private DataSource dataSource;
/** Query builder. */
private JdbcQueryBuilder queryBuilder;
/** Mapper. */
private JdbcPropertyMapper JDBC_PROPERTY_MAPPER = new JdbcPropertyMapper();
/** Mapper. */
private JdbcFeatureMapper JDBC_FEATURE_MAPPER = new JdbcFeatureMapper();
/** Default Constructor. */
public JdbcFeatureStore() {}
/**
* Constructor from DataSource.
*
* @param jdbcDS
* native jdbc datasource
*/
public JdbcFeatureStore(DataSource jdbcDS) {
this.dataSource = jdbcDS;
}
/**
* Constructor from DataSource.
*
* @param jdbcDS
* native jdbc datasource
*/
public JdbcFeatureStore(DataSource jdbcDS, String xmlConfFile) {
this(jdbcDS);
importFeaturesFromXmlFile(xmlConfFile);
}
/** {@inheritDoc} */
@Override
public void createSchema() {
DataSource ds = getDataSource();
JdbcQueryBuilder qb = getQueryBuilder();
if (!isTableExist(ds, qb.getTableNameFeatures())) {
executeUpdate(ds, qb.sqlCreateTableFeatures());
}
if (!isTableExist(ds, qb.getTableNameCustomProperties())) {
executeUpdate(ds, qb.sqlCreateTableCustomProperties());
}
if (!isTableExist(ds, qb.getTableNameRoles())) {
executeUpdate(ds, qb.sqlCreateTableRoles());
}
}
/** {@inheritDoc} */
@Override
public void enable(String uid) {
assertFeatureExist(uid);
update(getQueryBuilder().enableFeature(), uid);
}
/** {@inheritDoc} */
@Override
public void disable(String uid) {
assertFeatureExist(uid);
update(getQueryBuilder().disableFeature(), uid);
}
/** {@inheritDoc} */
@Override
public boolean exist(String uid) {
assertHasLength(uid);
Connection sqlConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
sqlConn = getDataSource().getConnection();
ps = JdbcUtils.buildStatement(sqlConn, getQueryBuilder().existFeature(), uid);
rs = ps.executeQuery();
rs.next();
return 1 == rs.getInt(1);
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeResultSet(rs);
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public Feature read(String uid) {
assertFeatureExist(uid);
Connection sqlConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
sqlConn = getDataSource().getConnection();
ps = sqlConn.prepareStatement(getQueryBuilder().getFeature());
ps.setString(1, uid);
rs = ps.executeQuery();
// Existence is tested before
rs.next();
Feature f = JDBC_FEATURE_MAPPER.mapFeature(rs);
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
// Enrich to get roles 2nd request
ps = sqlConn.prepareStatement(getQueryBuilder().getRoles());
ps.setString(1, uid);
rs = ps.executeQuery();
while (rs.next()) {
f.getPermissions().add(rs.getString("ROLE_NAME"));
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
// Enrich with properties 3d request to get custom properties by uid
ps = sqlConn.prepareStatement(getQueryBuilder().getFeatureProperties());
ps.setString(1, uid);
rs = ps.executeQuery();
while (rs.next()) {
f.addProperty(JDBC_PROPERTY_MAPPER.map(rs));
}
return f;
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeResultSet(rs);
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public void create(Feature fp) {
assertFeatureNotNull(fp);
Connection sqlConn = null;
PreparedStatement ps = null;
Boolean previousAutoCommit = null;
try {
// Create connection
sqlConn = getDataSource().getConnection();
if (exist(fp.getUid())) {
throw new FeatureAlreadyExistException(fp.getUid());
}
// Begin TX
previousAutoCommit = sqlConn.getAutoCommit();
sqlConn.setAutoCommit(false);
// Create feature
ps = sqlConn.prepareStatement(getQueryBuilder().createFeature());
ps.setString(1, fp.getUid());
ps.setInt(2, fp.isEnable() ? 1 : 0);
ps.setString(3, fp.getDescription());
String strategyColumn = null;
String expressionColumn = null;
if (fp.getFlippingStrategy() != null) {
strategyColumn = fp.getFlippingStrategy().getClass().getCanonicalName();
expressionColumn = MappingUtil.fromMap(fp.getFlippingStrategy().getInitParams());
}
ps.setString(4, strategyColumn);
ps.setString(5, expressionColumn);
ps.setString(6, fp.getGroup());
ps.executeUpdate();
closeStatement(ps);
ps = null;
// Create roles
for (String role : fp.getPermissions()) {
ps = sqlConn.prepareStatement(getQueryBuilder().addRoleToFeature());
ps.setString(1, fp.getUid());
ps.setString(2, role);
ps.executeUpdate();
closeStatement(ps);
ps = null;
}
// Create customproperties
if (fp.getCustomProperties() != null && !fp.getCustomProperties().isEmpty()) {
for (Property<?> pp : fp.getCustomProperties().values()) {
ps = createCustomProperty(sqlConn, fp.getUid(), pp);
closeStatement(ps);
ps = null;
}
}
// Commit
sqlConn.commit();
} catch (SQLException sqlEX) {
rollback(sqlConn);
throw new FeatureAccessException(CANNOT_UPDATE_FEATURES_DATABASE_SQL_ERROR, sqlEX);
} finally {
closeStatement(ps);
closeConnection(sqlConn, previousAutoCommit);
}
}
/** {@inheritDoc} */
@Override
public void delete(String uid) {
assertFeatureExist(uid);
Connection sqlConn = null;
PreparedStatement ps = null;
Boolean previousAutoCommit = null;
try {
// Create connection
sqlConn = getDataSource().getConnection();
previousAutoCommit = sqlConn.getAutoCommit();
sqlConn.setAutoCommit(false);
Feature fp = read(uid);
// Delete Properties
if (fp.getCustomProperties() != null) {
for (String property : fp.getCustomProperties().keySet()) {
ps = sqlConn.prepareStatement(getQueryBuilder().deleteFeatureProperty());
ps.setString(1, property);
ps.setString(2, fp.getUid());
ps.executeUpdate();
closeStatement(ps);
ps = null;
}
}
// Delete Roles
if (fp.getPermissions() != null) {
for (String role : fp.getPermissions()) {
ps = sqlConn.prepareStatement(getQueryBuilder().deleteFeatureRole());
ps.setString(1, fp.getUid());
ps.setString(2, role);
ps.executeUpdate();
closeStatement(ps);
ps = null;
}
}
// Delete Feature
ps = sqlConn.prepareStatement(getQueryBuilder().deleteFeature());
ps.setString(1, fp.getUid());
ps.executeUpdate();
closeStatement(ps);
ps = null;
// Commit
sqlConn.commit();
} catch (SQLException sqlEX) {
rollback(sqlConn);
throw new FeatureAccessException(CANNOT_UPDATE_FEATURES_DATABASE_SQL_ERROR, sqlEX);
} finally {
closeStatement(ps);
closeConnection(sqlConn, previousAutoCommit);
}
}
/** {@inheritDoc} */
@Override
public void grantRoleOnFeature(String uid, String roleName) {
assertFeatureExist(uid);
assertHasLength(roleName);
update(getQueryBuilder().addRoleToFeature(), uid, roleName);
}
/** {@inheritDoc} */
@Override
public void removeRoleFromFeature(String uid, String roleName) {
assertFeatureExist(uid);
assertHasLength(roleName);
update(getQueryBuilder().deleteFeatureRole(), uid, roleName);
}
/** {@inheritDoc} */
@Override
public Map<String, Feature> readAll() {
LinkedHashMap<String, Feature> mapFP = new LinkedHashMap<String, Feature>();
Connection sqlConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// Returns features
sqlConn = dataSource.getConnection();
ps = sqlConn.prepareStatement(getQueryBuilder().getAllFeatures());
rs = ps.executeQuery();
while (rs.next()) {
Feature f = JDBC_FEATURE_MAPPER.mapFeature(rs);
mapFP.put(f.getUid(), f);
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
// Returns Roles
ps = sqlConn.prepareStatement(getQueryBuilder().getAllRoles());
rs = ps.executeQuery();
while (rs.next()) {
String uid = rs.getString(COL_ROLE_FEATID);
mapFP.get(uid).getPermissions().add(rs.getString(COL_ROLE_ROLENAME));
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
// Read custom properties for each feature
for (Feature f : mapFP.values()) {
ps = sqlConn.prepareStatement(getQueryBuilder().getFeatureProperties());
ps.setString(1, f.getUid());
rs = ps.executeQuery();
while (rs.next()) {
f.addProperty(JDBC_PROPERTY_MAPPER.map(rs));
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
}
return mapFP;
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeResultSet(rs);
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public Set<String> readAllGroups() {
Set<String> setOFGroup = new HashSet<String>();
Connection sqlConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// Returns features
sqlConn = dataSource.getConnection();
ps = sqlConn.prepareStatement(getQueryBuilder().getAllGroups());
rs = ps.executeQuery();
while (rs.next()) {
String groupName = rs.getString(COL_FEAT_GROUPNAME);
if (Util.hasLength(groupName)) {
setOFGroup.add(groupName);
}
}
return setOFGroup;
} catch (SQLException sqlEX) {
throw new FeatureAccessException("Cannot list groups, error related to database", sqlEX);
} finally {
closeResultSet(rs);
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public void update(Feature fp) {
assertFeatureNotNull(fp);
Connection sqlConn = null;
PreparedStatement ps = null;
try {
sqlConn = dataSource.getConnection();
Feature fpExist = read(fp.getUid());
String enable = "0";
if (fp.isEnable()) {
enable = "1";
}
String fStrategy = null;
String fExpression = null;
if (fp.getFlippingStrategy() != null) {
fStrategy = fp.getFlippingStrategy().getClass().getCanonicalName();
fExpression = MappingUtil.fromMap(fp.getFlippingStrategy().getInitParams());
}
update(getQueryBuilder().updateFeature(),
enable, fp.getDescription(), fStrategy, fExpression, fp.getGroup(), fp.getUid());
// ROLES
// To be deleted (not in new value but was at first)
Set<String> toBeDeleted = new HashSet<String>();
toBeDeleted.addAll(fpExist.getPermissions());
toBeDeleted.removeAll(fp.getPermissions());
for (String roleToBeDelete : toBeDeleted) {
removeRoleFromFeature(fpExist.getUid(), roleToBeDelete);
}
// To be created : in second but not in first
Set<String> toBeAdded = new HashSet<String>();
toBeAdded.addAll(fp.getPermissions());
toBeAdded.removeAll(fpExist.getPermissions());
for (String addee : toBeAdded) {
grantRoleOnFeature(fpExist.getUid(), addee);
}
// REMOVE EXISTING CUSTOM PROPERTIES
ps = sqlConn.prepareStatement(getQueryBuilder().deleteAllFeatureCustomProperties());
ps.setString(1, fpExist.getUid());
ps.executeUpdate();
closeStatement(ps);
ps = null;
// CREATE CUSTOM PROPERTIES
for (Property<?> property : fp.getCustomProperties().values()) {
ps = createCustomProperty(sqlConn, fp.getUid(), property);
closeStatement(ps);
ps = null;
}
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public void clear() {
Connection sqlConn = null;
PreparedStatement ps = null;
try {
sqlConn = dataSource.getConnection();
ps = sqlConn.prepareStatement(getQueryBuilder().deleteAllCustomProperties());
ps.executeUpdate();
closeStatement(ps);
ps = null;
ps = sqlConn.prepareStatement(getQueryBuilder().deleteAllRoles());
ps.executeUpdate();
closeStatement(ps);
ps = null;
ps = sqlConn.prepareStatement(getQueryBuilder().deleteAllFeatures());
ps.executeUpdate();
closeStatement(ps);
ps = null;
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeStatement(ps);
closeConnection(sqlConn);
}
}
/**
* Ease creation of properties in Database.
*
* @param uid
* target unique identifier
* @param props
* target properties.
*/
public void createCustomProperties(String uid, Collection <Property<?> > props) {
Util.assertNotNull(uid);
if (props == null) return;
Connection sqlConn = null;
PreparedStatement ps = null;
Boolean previousAutoCommit = null;
try {
sqlConn = dataSource.getConnection();
// Begin TX
previousAutoCommit = sqlConn.getAutoCommit();
sqlConn.setAutoCommit(false);
// Queries
for (Property<?> pp : props) {
ps = createCustomProperty(sqlConn, uid, pp);
closeStatement(ps);
ps = null;
}
// End TX
sqlConn.commit();
} catch (SQLException sqlEX) {
rollback(sqlConn);
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeStatement(ps);
closeConnection(sqlConn, previousAutoCommit);
}
}
/**
* Create SQL statement to create property.
*
* @param sqlConn
* current sql connection
* @param featureId
* current unique feature identifier
* @param pp
* pojo property
* @return
* statement sql to be executed
* @throws SQLException
* error during sql operation
*/
private PreparedStatement createCustomProperty(Connection sqlConn, String featureId, Property<?> pp)
throws SQLException {
PreparedStatement ps = sqlConn.prepareStatement(getQueryBuilder().createFeatureProperty());
ps.setString(1, pp.getName());
ps.setString(2, pp.getType());
ps.setString(3, pp.asString());
ps.setString(4, pp.getDescription());
if (pp.getFixedValues() != null && !pp.getFixedValues().isEmpty()) {
String fixedValues = pp.getFixedValues().toString();
ps.setString(5, fixedValues.substring(1, fixedValues.length() - 1));
} else {
ps.setString(5, null);
}
ps.setString(6, featureId);
ps.executeUpdate();
return ps;
}
/** {@inheritDoc} */
@Override
public boolean existGroup(String groupName) {
assertHasLength(groupName);
Connection sqlConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
sqlConn = dataSource.getConnection();
ps = sqlConn.prepareStatement(getQueryBuilder().existGroup());
ps.setString(1, groupName);
rs = ps.executeQuery();
rs.next();
return rs.getInt(1) > 0;
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeResultSet(rs);
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public void enableGroup(String groupName) {
assertGroupExist(groupName);
update(getQueryBuilder().enableGroup(), groupName);
}
/** {@inheritDoc} */
@Override
public void disableGroup(String groupName) {
assertGroupExist(groupName);
update(getQueryBuilder().disableGroup(), groupName);
}
/** {@inheritDoc} */
@Override
public Map<String, Feature> readGroup(String groupName) {
assertGroupExist(groupName);
LinkedHashMap<String, Feature> mapFP = new LinkedHashMap<String, Feature>();
Connection sqlConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// Returns features
sqlConn = dataSource.getConnection();
ps = sqlConn.prepareStatement(getQueryBuilder().getFeatureOfGroup());
ps.setString(1, groupName);
rs = ps.executeQuery();
while (rs.next()) {
Feature f = JDBC_FEATURE_MAPPER.mapFeature(rs);
mapFP.put(f.getUid(), f);
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
// Returns Roles
ps = sqlConn.prepareStatement(getQueryBuilder().getAllRoles());
rs = ps.executeQuery();
while (rs.next()) {
String uid = rs.getString(COL_ROLE_FEATID);
// only feature in the group must be processed
if (mapFP.containsKey(uid)) {
mapFP.get(uid).getPermissions().add(rs.getString(COL_ROLE_ROLENAME));
}
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
// Read custom properties for each feature
for (Feature f : mapFP.values()) {
ps = sqlConn.prepareStatement(getQueryBuilder().getFeatureProperties());
ps.setString(1, f.getUid());
rs = ps.executeQuery();
while (rs.next()) {
f.addProperty(JDBC_PROPERTY_MAPPER.map(rs));
}
closeResultSet(rs);
rs = null;
closeStatement(ps);
ps = null;
}
return mapFP;
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_CHECK_FEATURE_EXISTENCE_ERROR_RELATED_TO_DATABASE, sqlEX);
} finally {
closeResultSet(rs);
closeStatement(ps);
closeConnection(sqlConn);
}
}
/** {@inheritDoc} */
@Override
public void addToGroup(String uid, String groupName) {
assertFeatureExist(uid);
assertHasLength(groupName);
update(getQueryBuilder().addFeatureToGroup(), groupName, uid);
}
/** {@inheritDoc} */
@Override
public void removeFromGroup(String uid, String groupName) {
assertFeatureExist(uid);
assertGroupExist(groupName);
Feature feat = read(uid);
if (feat.getGroup() != null && !feat.getGroup().equals(groupName)) {
throw new IllegalArgumentException("'" + uid + "' is not in group '" + groupName + "'");
}
update(getQueryBuilder().addFeatureToGroup(), "", uid);
}
/**
* Utility method to perform UPDATE and DELETE operations.
*
* @param query
* target query
* @param params
* sql query params
*/
public void update(String query, String... params) {
Connection sqlConnection = null;
PreparedStatement ps = null;
try {
sqlConnection = dataSource.getConnection();
ps = buildStatement(sqlConnection, query, params);
ps.executeUpdate();
} catch (SQLException sqlEX) {
throw new FeatureAccessException(CANNOT_UPDATE_FEATURES_DATABASE_SQL_ERROR, sqlEX);
} finally {
closeStatement(ps);
closeConnection(sqlConnection);
}
}
/**
* Getter accessor for attribute 'dataSource'.
*
* @return current value of 'dataSource'
*/
public DataSource getDataSource() {
if (dataSource == null) {
throw new IllegalStateException("DataSource has not been initialized");
}
return dataSource;
}
/**
* Setter accessor for attribute 'dataSource'.
*
* @param dataSource
* new value for 'dataSource '
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* @return the queryBuilder
*/
public JdbcQueryBuilder getQueryBuilder() {
if (queryBuilder == null) {
queryBuilder = new JdbcQueryBuilder();
}
return queryBuilder;
}
/**
* @param queryBuilder the queryBuilder to set
*/
public void setQueryBuilder(JdbcQueryBuilder queryBuilder) {
this.queryBuilder = queryBuilder;
}
}