/**
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.masterdb.user;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.threeten.bp.Instant;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.collect.ImmutableList;
import com.opengamma.DataDuplicationException;
import com.opengamma.DataVersionException;
import com.opengamma.core.user.UserAccount;
import com.opengamma.core.user.impl.SimpleUserAccount;
import com.opengamma.elsql.ElSqlBundle;
import com.opengamma.id.ObjectId;
import com.opengamma.id.UniqueId;
import com.opengamma.master.user.HistoryEvent;
import com.opengamma.master.user.HistoryEventType;
import com.opengamma.master.user.ManageableRole;
import com.opengamma.master.user.RoleEventHistoryRequest;
import com.opengamma.master.user.RoleEventHistoryResult;
import com.opengamma.master.user.RoleMaster;
import com.opengamma.master.user.RoleSearchRequest;
import com.opengamma.master.user.RoleSearchResult;
import com.opengamma.master.user.RoleSearchSortOrder;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.db.DbConnector;
import com.opengamma.util.db.DbMapSqlParameterSource;
import com.opengamma.util.paging.Paging;
import com.opengamma.util.paging.PagingRequest;
import com.opengamma.util.tuple.Pair;
import com.opengamma.util.tuple.Pairs;
/**
* A role master implementation using a database for persistence.
* <p>
* This is a full implementation of the role master using an SQL database.
* Full details of the API are in {@link RoleMaster}.
* <p>
* The SQL is stored externally in {@code DbRoleMaster.elsql}.
* Alternate databases or specific SQL requirements can be handled using database
* specific overrides, such as {@code DbRoleMaster-MySpecialDB.elsql}.
* <p>
* This class is mutable but must be treated as immutable after configuration.
*/
public class DbRoleMaster
extends AbstractDbUserMaster<ManageableRole>
implements RoleMaster {
/** Event sequence name. */
private static final String USR_ROLE_EVENT_SEQ = "usr_role_event_seq";
/** Logger. */
private static final Logger s_logger = LoggerFactory.getLogger(DbRoleMaster.class);
/**
* The default scheme for unique identifiers.
*/
public static final String IDENTIFIER_SCHEME_DEFAULT = "DbUsrRole";
/**
* SQL order by.
*/
protected static final EnumMap<RoleSearchSortOrder, String> ORDER_BY_MAP = new EnumMap<RoleSearchSortOrder, String>(RoleSearchSortOrder.class);
static {
ORDER_BY_MAP.put(RoleSearchSortOrder.OBJECT_ID_ASC, "oid ASC");
ORDER_BY_MAP.put(RoleSearchSortOrder.OBJECT_ID_DESC, "oid DESC");
ORDER_BY_MAP.put(RoleSearchSortOrder.NAME_ASC, "role_name ASC");
ORDER_BY_MAP.put(RoleSearchSortOrder.NAME_DESC, "role_name DESC");
}
// -----------------------------------------------------------------
// TIMERS FOR METRICS GATHERING
// By default these do nothing. Registration will replace them
// so that they actually do something.
// -----------------------------------------------------------------
private Timer _searchTimer = new Timer();
/**
* Creates an instance.
*
* @param dbConnector the database connector, not null
*/
public DbRoleMaster(final DbConnector dbConnector) {
super(dbConnector, IDENTIFIER_SCHEME_DEFAULT);
setElSqlBundle(ElSqlBundle.of(dbConnector.getDialect().getElSqlConfig(), DbRoleMaster.class));
}
@Override
public void registerMetrics(MetricRegistry summaryRegistry, MetricRegistry detailedRegistry, String namePrefix) {
super.registerMetrics(summaryRegistry, detailedRegistry, namePrefix);
_searchTimer = summaryRegistry.timer(namePrefix + ".search");
}
//-------------------------------------------------------------------------
@Override
public boolean nameExists(String roleName) {
ArgumentChecker.notNull(roleName, "roleName");
return doNameExists(roleName);
}
@Override
public ManageableRole getByName(String roleName) {
ArgumentChecker.notNull(roleName, "roleName");
s_logger.debug("getByName {}", roleName);
ObjectId oid = lookupName(roleName, OnDeleted.EXCEPTION);
return doGetById(oid, new RoleExtractor());
}
@Override
public ManageableRole getById(ObjectId objectId) {
ArgumentChecker.notNull(objectId, "objectId");
s_logger.debug("getById {}", objectId);
checkScheme(objectId);
return doGetById(objectId, new RoleExtractor());
}
//-------------------------------------------------------------------------
@Override
public UniqueId add(final ManageableRole role) {
return doAdd(role);
}
/**
* Processes the role add, within a retrying transaction.
*
* @param role the role to add, not null
* @return the information, not null
*/
@Override
Pair<UniqueId, Instant> doAddInTransaction(ManageableRole role) {
// check if role exists
if (doNameExists(role.getRoleName())) {
throw new DataDuplicationException("Role already exists: " + role.getRoleName());
}
// insert new row
final Instant now = now();
final long docOid = nextId("usr_role_seq");
final UniqueId uniqueId = createUniqueId(docOid, docOid);
insertMain(docOid, role);
insertNameLookup(role.getRoleName(), uniqueId.getObjectId());
insertAssociatedUsers(docOid, role);
insertAssociatedPermissions(docOid, role);
insertAssociatedRoles(docOid, role);
HistoryEvent event = HistoryEvent.of(HistoryEventType.ADDED, uniqueId, "system", now, ImmutableList.<String>of());
insertEvent(event, USR_ROLE_EVENT_SEQ);
return Pairs.of(uniqueId, now);
}
//-------------------------------------------------------------------------
@Override
public UniqueId update(final ManageableRole role) {
return doUpdate(role);
}
/**
* Processes the update, within a retrying transaction.
*
* @param role the updated role, not null
* @return the updated document, not null
*/
@Override
Pair<UniqueId, Instant> doUpdateInTransaction(ManageableRole role) {
ObjectId objectId = role.getObjectId();
String oldVersion = role.getUniqueId().getVersion();
ManageableRole current = getById(objectId);
int newVersion = Integer.parseInt(oldVersion) + 1;
UniqueId newUniqueId = objectId.atVersion(Integer.toString(newVersion));
// validate
if (current.equals(role)) {
return Pairs.of(newUniqueId, null); // no change
}
if (current.getUniqueId().getVersion().equals(oldVersion) == false) {
throw new DataVersionException("Invalid version, Role has already been updated: " + objectId);
}
if (caseInsensitive(role.getRoleName()).equals(caseInsensitive(current.getRoleName())) == false) {
// check if role exists
if (doNameExists(role.getRoleName())) {
throw new DataDuplicationException("Role cannot be renamed, new name already exists: " + role.getRoleName());
}
insertNameLookup(role.getRoleName(), current.getObjectId());
}
// update
long docOid = extractOid(objectId);
updateMain(docOid, newVersion, role);
if (current.getAssociatedUsers().equals(role.getAssociatedUsers()) == false) {
deleteAssociatedUsers(docOid);
insertAssociatedUsers(docOid, role);
}
if (current.getAssociatedPermissions().equals(role.getAssociatedPermissions()) == false) {
deleteAssociatedPermissions(docOid);
insertAssociatedPermissions(docOid, role);
}
if (current.getAssociatedRoles().equals(role.getAssociatedRoles()) == false) {
deleteAssociatedRoles(docOid);
insertAssociatedRoles(docOid, role);
}
final Instant now = now();
List<String> changes = calculateChanges(current, role);
HistoryEvent event = HistoryEvent.of(HistoryEventType.CHANGED, newUniqueId, "system", now, changes);
insertEvent(event, USR_ROLE_EVENT_SEQ);
return Pairs.of(newUniqueId, now);
}
private List<String> calculateChanges(ManageableRole current, ManageableRole updated) {
List<String> changes = new ArrayList<>();
// changes
createChange(changes, current, updated, ManageableRole.meta().roleName());
createChange(changes, current, updated, ManageableRole.meta().description());
// added permission
Set<String> addedUsers = new TreeSet<>(updated.getAssociatedUsers());
addedUsers.removeAll(current.getAssociatedUsers());
for (String permission : addedUsers) {
changes.add(StringUtils.left("Added user: " + permission, 255));
}
// removed permission
Set<String> removedUsers = new TreeSet<>(current.getAssociatedUsers());
removedUsers.removeAll(updated.getAssociatedUsers());
for (String permission : removedUsers) {
changes.add(StringUtils.left("Removed user: " + permission, 255));
}
// added permission
Set<String> addedPermissions = new TreeSet<>(updated.getAssociatedPermissions());
addedPermissions.removeAll(current.getAssociatedPermissions());
for (String permission : addedPermissions) {
changes.add(StringUtils.left("Added permission: " + permission, 255));
}
// removed permission
Set<String> removedPermissions = new TreeSet<>(current.getAssociatedPermissions());
removedPermissions.removeAll(updated.getAssociatedPermissions());
for (String permission : removedPermissions) {
changes.add(StringUtils.left("Removed permission: " + permission, 255));
}
// added permission
Set<String> addedRoles = new TreeSet<>(updated.getAssociatedRoles());
addedRoles.removeAll(current.getAssociatedRoles());
for (String permission : addedRoles) {
changes.add(StringUtils.left("Added role: " + permission, 255));
}
// removed permission
Set<String> removedRoles = new TreeSet<>(current.getAssociatedRoles());
removedRoles.removeAll(updated.getAssociatedRoles());
for (String permission : removedRoles) {
changes.add(StringUtils.left("Removed role: " + permission, 255));
}
return changes;
}
//-------------------------------------------------------------------------
@Override
public UniqueId save(ManageableRole role) {
ArgumentChecker.notNull(role, "role");
s_logger.debug("save {}", role.getRoleName());
if (role.getUniqueId() != null) {
return update(role);
} else {
return add(role);
}
}
//-------------------------------------------------------------------------
@Override
public void removeByName(String roleName) {
doRemoveByName(roleName);
}
@Override
public void removeById(final ObjectId objectId) {
doRemoveById(objectId);
}
/**
* Processes the document update, within a retrying transaction.
*
* @param objectId the object identifier to remove, not null
* @return the updated document, not null
*/
@Override
Instant doRemoveInTransaction(final ObjectId objectId) {
ManageableRole current = getById(objectId);
int newVersion = Integer.parseInt(current.getUniqueId().getVersion()) + 1;
UniqueId newUniqueId = objectId.atVersion(Integer.toString(newVersion));
long docOid = extractOid(objectId);
deleteAssociatedUsers(docOid);
deleteAssociatedPermissions(docOid);
deleteAssociatedRoles(docOid);
deleteMain(docOid);
updateNameLookupToDeleted(docOid);
Instant now = now();
HistoryEvent event = HistoryEvent.of(HistoryEventType.REMOVED, newUniqueId, "system", now, ImmutableList.<String>of());
insertEvent(event, USR_ROLE_EVENT_SEQ);
return now;
}
//-------------------------------------------------------------------------
@Override
public RoleSearchResult search(RoleSearchRequest request) {
ArgumentChecker.notNull(request, "request");
ArgumentChecker.notNull(request.getPagingRequest(), "request.pagingRequest");
s_logger.debug("search {}", request);
if ((request.getObjectIds() != null && request.getObjectIds().isEmpty())) {
Paging paging = Paging.of(request.getPagingRequest(), 0);
return new RoleSearchResult(paging, new ArrayList<ManageableRole>());
}
try (Timer.Context context = _searchTimer.time()) {
return doSearch(request);
}
}
private RoleSearchResult doSearch(RoleSearchRequest request) {
PagingRequest pagingRequest = request.getPagingRequest();
// setup args
final DbMapSqlParameterSource args = createParameterSource()
.addValueNullIgnored("role_name_ci", caseInsensitive(getDialect().sqlWildcardAdjustValue(request.getRoleName())))
.addValueNullIgnored("assoc_user", request.getAssociatedUser())
.addValueNullIgnored("assoc_perm", request.getAssociatedPermission())
.addValueNullIgnored("assoc_role", request.getAssociatedRole());
if (request.getObjectIds() != null) {
StringBuilder buf = new StringBuilder(request.getObjectIds().size() * 10);
for (ObjectId objectId : request.getObjectIds()) {
checkScheme(objectId);
buf.append(extractOid(objectId)).append(", ");
}
buf.setLength(buf.length() - 2);
args.addValue("sql_search_object_ids", buf.toString());
}
args.addValue("sort_order", ORDER_BY_MAP.get(request.getSortOrder()));
args.addValue("paging_offset", pagingRequest.getFirstItem());
args.addValue("paging_fetch", pagingRequest.getPagingSize());
// search
String[] sql = {getElSqlBundle().getSql("Search", args), getElSqlBundle().getSql("SearchCount", args)};
final NamedParameterJdbcOperations namedJdbc = getJdbcTemplate();
Paging paging;
List<ManageableRole> results = new ArrayList<>();
if (pagingRequest.equals(PagingRequest.ALL)) {
paging = Paging.of(pagingRequest, results);
results.addAll(namedJdbc.query(sql[0], args, new RoleExtractor()));
} else {
s_logger.debug("executing sql {}", sql[1]);
final int count = namedJdbc.queryForObject(sql[1], args, Integer.class);
paging = Paging.of(pagingRequest, count);
if (count > 0 && pagingRequest.equals(PagingRequest.NONE) == false) {
s_logger.debug("executing sql {}", sql[0]);
results.addAll(namedJdbc.query(sql[0], args, new RoleExtractor()));
}
}
return new RoleSearchResult(paging, results);
}
//-------------------------------------------------------------------------
@Override
public RoleEventHistoryResult eventHistory(RoleEventHistoryRequest request) {
ArgumentChecker.notNull(request, "request");
s_logger.debug("eventHistory {}", request);
ObjectId objectId = request.getObjectId();
if (objectId == null) {
objectId = lookupName(request.getRoleName(), OnDeleted.RETURN_ID);
}
checkScheme(objectId);
return new RoleEventHistoryResult(doEventHistory(objectId));
}
//-------------------------------------------------------------------------
@Override
public UserAccount resolveAccount(UserAccount account) {
ArgumentChecker.notNull(account, "account");
SimpleUserAccount resolved = SimpleUserAccount.from(account);
final DbMapSqlParameterSource args = createParameterSource()
.addValue("user_name_ci", caseInsensitive(account.getUserName()));
final String sql = getElSqlBundle().getSql("GetResolvedRoles", args);
List<Map<String, Object>> result = getJdbcTemplate().queryForList(sql, args);
for (Map<String, Object> row : result) {
Object role = row.get("ROLE_NAME");
if (role != null) {
resolved.getRoles().add(role.toString());
Object perm = row.get("ASSOC_PERM");
if (perm != null) {
resolved.getPermissions().add(perm.toString());
}
}
}
return resolved;
}
//-------------------------------------------------------------------------
private void insertMain(long docOid, ManageableRole role) {
final DbMapSqlParameterSource docArgs = mainArgs(docOid, 0, role);
final String sqlDoc = getElSqlBundle().getSql("InsertMain", docArgs);
getJdbcTemplate().update(sqlDoc, docArgs);
}
private void insertAssociatedUsers(long docOid, ManageableRole role) {
final List<DbMapSqlParameterSource> argsList = new ArrayList<DbMapSqlParameterSource>();
for (String assoc : role.getAssociatedUsers()) {
argsList.add(createParameterSource()
.addValue("id", nextId("usr_role_assocuser_seq"))
.addValue("doc_id", docOid)
.addValue("assoc_user", caseInsensitive(assoc)));
}
final String sql = getElSqlBundle().getSql("InsertAssocUser");
getJdbcTemplate().batchUpdate(sql, argsList.toArray(new DbMapSqlParameterSource[argsList.size()]));
}
private void insertAssociatedPermissions(long docOid, ManageableRole role) {
final List<DbMapSqlParameterSource> argsList = new ArrayList<DbMapSqlParameterSource>();
for (String assoc : role.getAssociatedPermissions()) {
argsList.add(createParameterSource()
.addValue("id", nextId("usr_role_assocperm_seq"))
.addValue("doc_id", docOid)
.addValue("assoc_perm", assoc));
}
final String sql = getElSqlBundle().getSql("InsertAssocPerm");
getJdbcTemplate().batchUpdate(sql, argsList.toArray(new DbMapSqlParameterSource[argsList.size()]));
}
private void insertAssociatedRoles(long docOid, ManageableRole role) {
final List<DbMapSqlParameterSource> argsList = new ArrayList<DbMapSqlParameterSource>();
for (String assoc : role.getAssociatedRoles()) {
argsList.add(createParameterSource()
.addValue("id", nextId("usr_role_assocrole_seq"))
.addValue("doc_id", docOid)
.addValue("assoc_role", caseInsensitive(assoc)));
}
final String sql = getElSqlBundle().getSql("InsertAssocRole");
getJdbcTemplate().batchUpdate(sql, argsList.toArray(new DbMapSqlParameterSource[argsList.size()]));
}
//-------------------------------------------------------------------------
private void updateMain(long docOid, int version, ManageableRole role) {
final DbMapSqlParameterSource docArgs = mainArgs(docOid, version, role);
final String sqlDoc = getElSqlBundle().getSql("UpdateMain", docArgs);
getJdbcTemplate().update(sqlDoc, docArgs);
}
private DbMapSqlParameterSource mainArgs(long docOid, int version, ManageableRole role) {
final DbMapSqlParameterSource docArgs = createParameterSource()
.addValue("doc_id", docOid)
.addValue("version", version)
.addValue("role_name", role.getRoleName())
.addValue("role_name_ci", caseInsensitive(role.getRoleName()))
.addValue("description", role.getDescription());
return docArgs;
}
//-------------------------------------------------------------------------
private void deleteMain(long docOid) {
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", docOid);
final String sql = getElSqlBundle().getSql("DeleteMain", args);
getJdbcTemplate().update(sql, args);
}
private void deleteAssociatedUsers(long docOid) {
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", docOid);
final String sql = getElSqlBundle().getSql("DeleteAssocUsers", args);
getJdbcTemplate().update(sql, args);
}
private void deleteAssociatedPermissions(long docOid) {
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", docOid);
final String sql = getElSqlBundle().getSql("DeleteAssocPerms", args);
getJdbcTemplate().update(sql, args);
}
private void deleteAssociatedRoles(long docOid) {
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", docOid);
final String sql = getElSqlBundle().getSql("DeleteAssocRoles", args);
getJdbcTemplate().update(sql, args);
}
//-------------------------------------------------------------------------
/**
* Mapper from SQL rows to a ManageableRole.
*/
final class RoleExtractor implements ResultSetExtractor<List<ManageableRole>> {
private long _previousDocId = -1L;
private ManageableRole _currRole;
private Set<String> _currUsers = new HashSet<>();
private Set<String> _currPermissions = new HashSet<>();
private Set<String> _currRoles = new HashSet<>();
private List<ManageableRole> _roles = new ArrayList<>();
@Override
public List<ManageableRole> extractData(final ResultSet rs) throws SQLException, DataAccessException {
while (rs.next()) {
final long docId = rs.getLong("DOC_ID");
// DOC_ID tells us when we're on a new document.
if (docId != _previousDocId) {
if (_previousDocId >= 0) {
_currRole.setAssociatedUsers(_currUsers);
_currRole.setAssociatedPermissions(_currPermissions);
_currRole.setAssociatedRoles(_currRoles);
}
_previousDocId = docId;
buildRole(rs, docId);
_currUsers.clear();
_currPermissions.clear();
_currRoles.clear();
}
String assocUser = rs.getString("ASSOC_USER");
if (assocUser != null) {
_currUsers.add(assocUser);
}
String assocPerm = rs.getString("ASSOC_PERM");
if (assocPerm != null) {
_currPermissions.add(assocPerm);
}
String assocRole = rs.getString("ASSOC_ROLE");
if (assocRole != null) {
_currRoles.add(assocRole);
}
}
// patch up last document read
if (_previousDocId >= 0) {
_currRole.setAssociatedUsers(_currUsers);
_currRole.setAssociatedPermissions(_currPermissions);
_currRole.setAssociatedRoles(_currRoles);
}
return _roles;
}
private void buildRole(final ResultSet rs, final long docId) throws SQLException {
int version = rs.getInt("VERSION");
UniqueId uniqueId = UniqueId.of(getUniqueIdScheme(), Long.toString(docId), Integer.toString(version));
ManageableRole role = new ManageableRole(rs.getString("ROLE_NAME"));
role.setUniqueId(uniqueId);
role.setDescription(rs.getString("DESCRIPTION"));
_currRole = role;
_roles.add(role);
}
}
}