/**
* 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.List;
import java.util.Locale;
import java.util.Objects;
import org.apache.commons.lang.StringUtils;
import org.joda.beans.Bean;
import org.joda.beans.MetaProperty;
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.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.threeten.bp.Instant;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.collect.ImmutableList;
import com.opengamma.DataNotFoundException;
import com.opengamma.core.change.BasicChangeManager;
import com.opengamma.core.change.ChangeManager;
import com.opengamma.core.change.ChangeType;
import com.opengamma.id.ObjectId;
import com.opengamma.id.UniqueId;
import com.opengamma.id.UniqueIdentifiable;
import com.opengamma.master.user.HistoryEvent;
import com.opengamma.master.user.HistoryEventType;
import com.opengamma.masterdb.AbstractDbMaster;
import com.opengamma.masterdb.ConfigurableDbChangeProvidingMaster;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.auth.AuthUtils;
import com.opengamma.util.db.DbConnector;
import com.opengamma.util.db.DbDateUtils;
import com.opengamma.util.db.DbMapSqlParameterSource;
import com.opengamma.util.metric.MetricProducer;
import com.opengamma.util.tuple.Pair;
/**
* Abstract master implementation using a database for persistence.
* <p>
* This class is mutable but must be treated as immutable after configuration.
*
* @param <T> the object type
*/
public abstract class AbstractDbUserMaster<T extends UniqueIdentifiable>
extends AbstractDbMaster
implements MetricProducer, ConfigurableDbChangeProvidingMaster {
/** Logger. */
private static final Logger s_logger = LoggerFactory.getLogger(AbstractDbUserMaster.class);
/**
* The change manager.
*/
private ChangeManager _changeManager = new BasicChangeManager();
// -----------------------------------------------------------------
// TIMERS FOR METRICS GATHERING
// By default these do nothing. Registration will replace them
// so that they actually do something.
// -----------------------------------------------------------------
private Timer _getByIdTimer = new Timer();
private Timer _lookupNameTimer = new Timer();
private Timer _addTimer = new Timer();
private Timer _updateTimer = new Timer();
private Timer _removeByIdTimer = new Timer();
private Timer _eventHistoryTimer = new Timer();
/**
* Creates an instance.
*
* @param dbConnector the database connector, not null
* @param defaultScheme the default unique identifier scheme, not null
*/
public AbstractDbUserMaster(final DbConnector dbConnector, final String defaultScheme) {
super(dbConnector, defaultScheme);
}
@Override
public void registerMetrics(MetricRegistry summaryRegistry, MetricRegistry detailedRegistry, String namePrefix) {
_getByIdTimer = summaryRegistry.timer(namePrefix + ".get");
_lookupNameTimer = summaryRegistry.timer(namePrefix + ".lookupname");
_addTimer = summaryRegistry.timer(namePrefix + ".add");
_updateTimer = summaryRegistry.timer(namePrefix + ".update");
_removeByIdTimer = summaryRegistry.timer(namePrefix + ".remove");
_eventHistoryTimer = summaryRegistry.timer(namePrefix + ".history");
}
//-------------------------------------------------------------------------
/**
* Gets the change manager.
*
* @return the change manager, not null
*/
@Override
public ChangeManager getChangeManager() {
return _changeManager;
}
/**
* Sets the change manager.
*
* @param changeManager the change manager, not null
*/
@Override
public void setChangeManager(final ChangeManager changeManager) {
ArgumentChecker.notNull(changeManager, "changeManager");
_changeManager = changeManager;
}
public ChangeManager changeManager() {
return _changeManager;
}
//-------------------------------------------------------------------------
/**
* Convert a name to an object identifier.
* <p>
* If an object is renamed, the old name remains as an alias.
* The separate name resolution handles that case.
*
* @param name the name, not null
* @param onDeleted how to handle deletion
* @return the object identifier, null if not found
*/
ObjectId lookupName(String name, OnDeleted onDeleted) {
ArgumentChecker.notNull(name, "name");
try (Timer.Context context = _lookupNameTimer.time()) {
final DbMapSqlParameterSource args = createParameterSource()
.addValue("name_ci", caseInsensitive(name));
final NamedParameterJdbcOperations namedJdbc = getDbConnector().getJdbcTemplate();
final String sql = getElSqlBundle().getSql("GetIdByName", args);
SqlRowSet rowSet = namedJdbc.queryForRowSet(sql, args);
if (rowSet.next() == false) {
throw new DataNotFoundException("Name not found: " + name);
}
String deleted = rowSet.getString("DELETED");
if (deleted.equals("Y")) {
if (onDeleted == OnDeleted.RETURN_NULL) {
return null;
} else if (onDeleted == OnDeleted.EXCEPTION) {
throw new DataNotFoundException("Name not found: " + name);
}
}
return createObjectId(rowSet.getLong("DOC_ID")).getObjectId();
}
}
/**
* Checks if a name exists already.
*
* @param name the name, not null
* @return true if exists
*/
boolean doNameExists(String name) {
ArgumentChecker.notNull(name, "name");
try (Timer.Context context = _lookupNameTimer.time()) {
final DbMapSqlParameterSource args = createParameterSource()
.addValue("name_ci", caseInsensitive(name));
final NamedParameterJdbcOperations namedJdbc = getDbConnector().getJdbcTemplate();
final String sql = getElSqlBundle().getSql("GetIdByName", args);
SqlRowSet rowSet = namedJdbc.queryForRowSet(sql, args);
return rowSet.next();
}
}
T doGetById(ObjectId objectId, ResultSetExtractor<List<T>> extractor) {
try (Timer.Context context = _getByIdTimer.time()) {
final long oid = extractOid(objectId);
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", oid);
final NamedParameterJdbcOperations namedJdbc = getDbConnector().getJdbcTemplate();
final String sql = getElSqlBundle().getSql("GetById", args);
final List<T> users = namedJdbc.query(sql, args, extractor);
if (users.isEmpty()) {
throw new DataNotFoundException("Identifier not found: " + objectId);
}
return users.get(0);
}
}
/**
* Checks if a user exists already.
*
* @param objectId the user identifier, not null
* @return true if exists
*/
boolean idExists(ObjectId objectId) {
final long oid = extractOid(objectId);
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", oid);
final NamedParameterJdbcOperations namedJdbc = getDbConnector().getJdbcTemplate();
final String sql = getElSqlBundle().getSql("GetById", args);
SqlRowSet rowSet = namedJdbc.queryForRowSet(sql, args);
return rowSet.next();
}
//-------------------------------------------------------------------------
UniqueId doAdd(final T user) {
ArgumentChecker.notNull(user, "user");
s_logger.debug("add {}", user);
try (Timer.Context context = _addTimer.time()) {
final Pair<UniqueId, Instant> added = getTransactionTemplateRetrying(getMaxRetries()).execute(new TransactionCallback<Pair<UniqueId, Instant>>() {
@Override
public Pair<UniqueId, Instant> doInTransaction(final TransactionStatus status) {
return doAddInTransaction(user);
}
});
changeManager().entityChanged(ChangeType.ADDED, added.getFirst().getObjectId(), added.getSecond(), null, added.getSecond());
return added.getFirst();
}
}
/**
* Processes the user add, within a retrying transaction.
*
* @param user the user to add, not null
* @return the information, not null
*/
abstract Pair<UniqueId, Instant> doAddInTransaction(T user);
//-------------------------------------------------------------------------
UniqueId doUpdate(final T user) {
ArgumentChecker.notNull(user, "user");
ArgumentChecker.notNull(user.getUniqueId(), "user.uniqueId");
ArgumentChecker.isTrue(user.getUniqueId().isVersioned(), "UniqueId must be versioned");
checkScheme(user.getUniqueId());
s_logger.debug("update {}", user);
try (Timer.Context context = _updateTimer.time()) {
final Pair<UniqueId, Instant> updated = getTransactionTemplateRetrying(getMaxRetries()).execute(new TransactionCallback<Pair<UniqueId, Instant>>() {
@Override
public Pair<UniqueId, Instant> doInTransaction(final TransactionStatus status) {
return doUpdateInTransaction(user);
}
});
if (updated.getSecond() != null) {
changeManager().entityChanged(ChangeType.CHANGED, updated.getFirst().getObjectId(), updated.getSecond(), null, updated.getSecond());
}
return updated.getFirst();
}
}
/**
* Processes the update, within a retrying transaction.
*
* @param user the updated user, not null
* @return the updated document, not null
*/
abstract Pair<UniqueId, Instant> doUpdateInTransaction(T user);
void createChange(List<String> changes, Bean current, Bean updated, MetaProperty<?> metaProperty) {
Object currentValue = metaProperty.get(current);
Object updatedValue = metaProperty.get(updated);
if (Objects.equals(currentValue, updatedValue) == false) {
String text = "Changed " + metaProperty.name() + ": " + currentValue + " -> " + updatedValue;
changes.add(StringUtils.left(text, 255));
}
}
//-------------------------------------------------------------------------
void doRemoveByName(String name) {
ArgumentChecker.notNull(name, "name");
s_logger.debug("removeByName {}", name);
ObjectId oid = lookupName(name, OnDeleted.RETURN_NULL);
if (oid == null) {
return; // already deleted
}
doRemoveById(oid);
}
void doRemoveById(final ObjectId objectId) {
ArgumentChecker.notNull(objectId, "objectId");
checkScheme(objectId);
s_logger.debug("removeById {}", objectId);
try (Timer.Context context = _removeByIdTimer.time()) {
if (idExists(objectId)) {
final Instant removedInstant = getTransactionTemplateRetrying(getMaxRetries()).execute(new TransactionCallback<Instant>() {
@Override
public Instant doInTransaction(final TransactionStatus status) {
return doRemoveInTransaction(objectId);
}
});
changeManager().entityChanged(ChangeType.REMOVED, objectId, removedInstant, null, removedInstant);
}
}
}
/**
* Processes the document update, within a retrying transaction.
*
* @param objectId the object identifier to remove, not null
* @return the updated document, not null
*/
abstract Instant doRemoveInTransaction(final ObjectId objectId);
List<HistoryEvent> doEventHistory(ObjectId objectId) {
try (Timer.Context context = _eventHistoryTimer.time()) {
final long oid = extractOid(objectId);
final DbMapSqlParameterSource args = createParameterSource()
.addValue("doc_id", oid);
final NamedParameterJdbcOperations namedJdbc = getDbConnector().getJdbcTemplate();
final String sql = getElSqlBundle().getSql("GetEventHistory", args);
return namedJdbc.query(sql, args, new EventExtractor());
}
}
//-------------------------------------------------------------------------
String caseInsensitive(String name) {
return name != null ? name.toLowerCase(Locale.ROOT) : name;
}
void insertNameLookup(String name, ObjectId objectId) {
final DbMapSqlParameterSource eventArgs = createParameterSource()
.addValue("name_ci", caseInsensitive(name))
.addValue("doc_id", extractOid(objectId));
final String sqlEvent = getElSqlBundle().getSql("InsertNameLookup");
getJdbcTemplate().update(sqlEvent, eventArgs);
}
void insertEvent(HistoryEvent event, String eventIdSequence) {
Long eventId = nextId(eventIdSequence);
String userName = AuthUtils.getUserName();
String activeUser = (userName != null ? userName : "system");
final DbMapSqlParameterSource eventArgs = createParameterSource()
.addValue("id", eventId)
.addValue("doc_id", extractOid(event.getUniqueId()))
.addValue("version", Integer.parseInt(event.getUniqueId().getVersion()))
.addValue("event_type", event.getType().name().substring(0, 1))
.addValue("active_user", activeUser)
.addValue("event_instant", DbDateUtils.toSqlTimestamp(event.getInstant()));
final String sqlEvent = getElSqlBundle().getSql("InsertEvent");
getJdbcTemplate().update(sqlEvent, eventArgs);
final List<DbMapSqlParameterSource> itemList = new ArrayList<DbMapSqlParameterSource>();
for (String description : event.getChanges()) {
final DbMapSqlParameterSource itemArgs = createParameterSource()
.addValue("id", nextId(eventIdSequence))
.addValue("event_id", eventId)
.addValue("description", description);
itemList.add(itemArgs);
}
final String sqlEventItem = getElSqlBundle().getSql("InsertEventItem");
getJdbcTemplate().batchUpdate(sqlEventItem, itemList.toArray(new DbMapSqlParameterSource[itemList.size()]));
}
void updateNameLookupToDeleted(long docOid) {
final DbMapSqlParameterSource docArgs = createParameterSource()
.addValue("doc_id", docOid);
final String sqlDoc = getElSqlBundle().getSql("UpdateNameLookupToDeleted", docArgs);
getJdbcTemplate().update(sqlDoc, docArgs);
}
/**
* Converts a single character to an enum value.
*
* @param typeStr the type character, not null
* @param values the enum values, not null
* @return the enum, not null
*/
<E extends Enum<E>> E extractEnum(String typeStr, E[] values) {
for (E t : values) {
if (typeStr.equals(t.name().substring(0, 1))) {
return t;
}
}
throw new IllegalStateException("Invalid enum value: " + typeStr);
}
//-------------------------------------------------------------------------
/**
* How to handle deletion.
*/
enum OnDeleted {
RETURN_NULL,
RETURN_ID,
EXCEPTION,
}
//-------------------------------------------------------------------------
/**
* Mapper from SQL rows to a HistoryEvent.
*/
final class EventExtractor implements ResultSetExtractor<List<HistoryEvent>> {
@Override
public List<HistoryEvent> extractData(final ResultSet rs) throws SQLException, DataAccessException {
List<HistoryEvent> events = new ArrayList<>();
long lastId = -1;
HistoryEvent current = null;
List<String> currentDescriptions = new ArrayList<>();
while (rs.next()) {
final long docId = rs.getLong("ID");
if (lastId != docId) {
if (current != null) {
events.add(current.toBuilder().changes(currentDescriptions).build());
currentDescriptions.clear();
}
lastId = docId;
current = buildEvent(rs);
}
final String description = rs.getString("DESCRIPTION");
if (description != null) {
currentDescriptions.add(description);
}
}
if (current != null) {
events.add(current.toBuilder().changes(currentDescriptions).build());
}
return events;
}
private HistoryEvent buildEvent(final ResultSet rs) throws SQLException {
long userId = rs.getInt("DOC_ID");
int version = rs.getInt("VERSION");
UniqueId uniqueId = UniqueId.of(getUniqueIdScheme(), Long.toString(userId), Integer.toString(version));
String typeStr = rs.getString("EVENT_TYPE");
HistoryEventType type = extractEnum(typeStr, HistoryEventType.values());
String activeUser = rs.getString("ACTIVE_USER");
Instant instant = DbDateUtils.fromSqlTimestamp(rs.getTimestamp("EVENT_INSTANT"));
return HistoryEvent.of(type, uniqueId, activeUser, instant, ImmutableList.<String>of());
}
}
}