/*
* Copyright (c) 2017 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.magma.datasource.hibernate;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import javax.validation.constraints.NotNull;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.criterion.Restrictions;
import org.obiba.core.service.impl.hibernate.AssociationCriteria;
import org.obiba.core.service.impl.hibernate.AssociationCriteria.Operation;
import org.obiba.magma.MagmaRuntimeException;
import org.obiba.magma.NoSuchValueTableException;
import org.obiba.magma.Timestamped;
import org.obiba.magma.Timestamps;
import org.obiba.magma.ValueTable;
import org.obiba.magma.ValueTableWriter;
import org.obiba.magma.datasource.hibernate.converter.AttributeAwareConverter;
import org.obiba.magma.datasource.hibernate.converter.HibernateMarshallingContext;
import org.obiba.magma.datasource.hibernate.domain.AttributeState;
import org.obiba.magma.datasource.hibernate.domain.DatasourceState;
import org.obiba.magma.datasource.hibernate.domain.ValueTableState;
import org.obiba.magma.datasource.hibernate.domain.VariableState;
import org.obiba.magma.support.AbstractDatasource;
import org.obiba.magma.support.UnionTimestamps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
/**
* Datasource based on entity-attribute-value model.
*/
public class HibernateDatasource extends AbstractDatasource {
private static final Logger log = LoggerFactory.getLogger(HibernateDatasource.class);
public static final String TYPE = "hibernate";
private final SessionFactory sessionFactory;
private Serializable datasourceId;
/**
* A Map of {@code org.hibernate.Transaction} to a list of all {@code HibernateValueTable} involved in the
* transaction. This map uses weak keys, meaning that when the transaction object is no longer reference anywhere, its
* entry is removed from the map.
*/
private final ConcurrentMap<Transaction, List<HibernateValueTableTransaction>> syncMap = new MapMaker().weakKeys()
.makeMap();
@SuppressWarnings("ConstantConditions")
public HibernateDatasource(@NotNull String name, @NotNull SessionFactory sessionFactory) {
super(name, TYPE);
if(sessionFactory == null) throw new IllegalArgumentException("sessionFactory cannot be null");
this.sessionFactory = sessionFactory;
}
/**
* Creates a writer for the specified table name and entity type. If the table does not exist, a new one is created.
* <p/>
* Note that a Hibernate transaction must be active for this method to return an instance of {@code ValueTableWriter}
*/
@NotNull
@Override
public ValueTableWriter createWriter(@NotNull String tableName, @NotNull String entityType) {
Preconditions.checkArgument(tableName != null, "tableName cannot be null");
Preconditions.checkArgument(entityType != null, "entityType cannot be null");
HibernateValueTableTransaction valueTableTransaction;
if(hasTableTransaction(tableName)) {
valueTableTransaction = getTableTransaction(tableName);
} else {
// A new transaction must be created. Either for an existing table or a new table.
if(super.hasValueTable(tableName)) {
// Create a transaction on an existing table
HibernateValueTable valueTable = (HibernateValueTable) getValueTable(tableName);
valueTableTransaction = newTableTransaction(valueTable, false);
} else {
// Create table transaction.
ValueTableState valueTableState = new ValueTableState(tableName, entityType, getDatasourceState());
getSessionFactory().getCurrentSession().save(valueTableState);
getSessionFactory().getCurrentSession().refresh(valueTableState); //OPAL-2635
// Create a transaction for a new table
valueTableTransaction = newTableTransaction(new HibernateValueTable(this, valueTableState), true);
}
}
return valueTableTransaction.getTransactionWriter();
}
@Override
public boolean isTransactional() {
return true;
}
/**
* Returns true if a value table exists for the specified name or that a create table transaction is active for that
* tableName.
*/
@Override
public boolean hasValueTable(String tableName) {
// If parent doesn't have the table, there may be an active transaction creating this table.
return super.hasValueTable(tableName) || hasTableTransaction(tableName);
}
/**
* Returns the table with the specified tableName. If a create table transaction is active, the table will be visible
* within the current transaction only.
*/
@Override
public ValueTable getValueTable(String tableName) throws NoSuchValueTableException {
if(hasTableTransaction(tableName)) {
return getTableTransaction(tableName).getValueTable();
}
return super.getValueTable(tableName);
}
@Override
public boolean canDropTable(String tableName) {
return hasValueTable(tableName);
}
@Override
public void dropTable(@NotNull String tableName) {
Stopwatch stopwatch = Stopwatch.createStarted();
String tableFullName = getName() + "." + tableName;
log.info("Dropping table {}", tableFullName);
HibernateValueTable valueTable = (HibernateValueTable) getValueTable(tableName);
ValueTableState tableState = valueTable.getValueTableState();
removeValueTable(tableName);
// cannot use cascading because DELETE (and INSERT) do not cascade via relationships in JPQL query
Session session = getSessionFactory().getCurrentSession();
deleteValueSets(tableFullName, session,
session.getNamedQuery("findValueSetIdsByTableId").setParameter("valueTableId", tableState.getId()).list());
deleteTableVariables(tableFullName, tableState, session);
session.delete(tableState);
log.debug("Dropped table '{}' in {}", tableFullName, stopwatch);
// force datasource timestamp update
updateDatasourceLastUpdate();
}
private void deleteTableVariables(String tableFullName, ValueTableState tableState, Session session) {
Stopwatch stopwatch = Stopwatch.createStarted();
List<VariableState> variables = AssociationCriteria.create(VariableState.class, session)
.add("valueTable", Operation.eq, tableState).list();
for(VariableState v : variables) {
session.delete(v);
}
log.debug("Deleted {} variables from {} in {}", variables.size(), tableFullName, stopwatch);
}
private void updateDatasourceLastUpdate() {
getDatasourceState().setUpdated(new Date());
}
@Override
public boolean canDrop() {
return true;
}
@Override
public void drop() {
for(String valueTable : getValueTableNames()) {
dropTable(valueTable);
}
sessionFactory.getCurrentSession().delete(getDatasourceState());
}
void deleteValueSets(String tableFullName, @SuppressWarnings("TypeMayBeWeakened") Session session,
Collection<?> valueSetIds) {
if(valueSetIds.isEmpty()) return;
Stopwatch stopwatch = Stopwatch.createStarted();
int nbDeletedBinaries = session.getNamedQuery("deleteValueSetBinaryValues")
.setParameterList("valueSetIds", valueSetIds).executeUpdate();
log.debug("Deleted {} binaries from {} in {}", nbDeletedBinaries, tableFullName, stopwatch.stop());
stopwatch.start();
int nbDeletedValues = session.getNamedQuery("deleteValueSetValues").setParameterList("valueSetIds", valueSetIds)
.executeUpdate();
log.debug("Deleted {} values from {} in {}", nbDeletedValues, tableFullName, stopwatch.stop());
stopwatch.start();
int nbDeletedValueSets = session.getNamedQuery("deleteValueSetStates")
.setParameterList("valueTableIds", valueSetIds).executeUpdate();
log.debug("Deleted {} valueSets from {} in {}", nbDeletedValueSets, tableFullName, stopwatch.stop());
}
@Override
protected void onInitialise() {
DatasourceState datasourceState = (DatasourceState) sessionFactory.getCurrentSession()
.createCriteria(DatasourceState.class).add(Restrictions.eq("name", getName())).uniqueResult();
// If datasource not persisted, create the persisted DatasourceState.
if(datasourceState == null) {
datasourceState = new DatasourceState(getName());
sessionFactory.getCurrentSession().save(datasourceState);
sessionFactory.getCurrentSession().refresh(datasourceState); //OPAL-2635
} else {
// If already persisted, load the persisted attributes for that datasource.
for(AttributeState attribute : datasourceState.getAttributes()) {
setAttributeValue(attribute.getName(), attribute.getValue());
}
}
datasourceId = datasourceState.getId();
}
@Override
protected void onDispose() {
DatasourceState state = getDatasourceState();
if(state != null) { // in case we dropped the datasource
new AttributeAwareConverter().addAttributes(this, state);
getSessionFactory().getCurrentSession().save(state);
}
}
@Override
protected Set<String> getValueTableNames() {
Set<String> names = new LinkedHashSet<>();
AssociationCriteria criteria = AssociationCriteria.create(ValueTableState.class, sessionFactory.getCurrentSession())
.add("datasource.id", Operation.eq, datasourceId);
for(Object obj : criteria.list()) {
ValueTableState state = (ValueTableState) obj;
names.add(state.getName());
}
return names;
}
@Override
protected ValueTable initialiseValueTable(String tableName) {
return new HibernateValueTable(this,
(ValueTableState) AssociationCriteria.create(ValueTableState.class, sessionFactory.getCurrentSession())
.add("datasource.id", Operation.eq, datasourceId) //
.add("name", Operation.eq, tableName) //
.getCriteria().uniqueResult());
}
@NotNull
@Override
public Timestamps getTimestamps() {
ImmutableSet.Builder<Timestamped> builder = ImmutableSet.builder();
builder.addAll(getValueTables()).add(getDatasourceState());
return new UnionTimestamps(builder.build());
}
@Override
public boolean canRenameTable(String tableName) {
return hasValueTable(tableName);
}
@Override
public void renameTable(String tableName, String newName) {
if(canRenameTable(newName)) throw new MagmaRuntimeException("A table already exists with the name: " + newName);
log.info("Renaming table {} to {}", tableName, newName);
((HibernateValueTable) getValueTable(tableName)).setName(newName);
updateDatasourceLastUpdate();
}
/**
* Adds the specified {@code ValueTable} to the set of value tables this datasource holds. This method is used by
* {@code HibernateValueTableTransaction} to add value tables that are created within a transaction.
*
* @param vt the value table instance to add
*/
void commitValueTable(ValueTable vt) {
addValueTable(vt);
}
/**
* Use with caution!
*
* @return sessionFactory used by the datasource
*/
public SessionFactory getSessionFactory() {
return sessionFactory;
}
DatasourceState getDatasourceState() {
return (DatasourceState) sessionFactory.getCurrentSession().get(DatasourceState.class, datasourceId);
}
@SuppressWarnings("UnusedDeclaration")
HibernateMarshallingContext createContext() {
return HibernateMarshallingContext.create(getSessionFactory(), getDatasourceState());
}
HibernateMarshallingContext createContext(ValueTableState valueTableState) {
return HibernateMarshallingContext.create(getSessionFactory(), getDatasourceState(), valueTableState);
}
/**
* Returns true if a transaction exists on the specified table name. False otherwise. Note that this will return true
* only if the current thread is associated with the transaction.
*
* @param name the name of the value table
* @return true when a {@code HibernateValueTableTransaction} currently exists for the specified table name
*/
boolean hasTableTransaction(String name) {
for(HibernateValueTableTransaction tableTx : lookupTableTransactions()) {
if(tableTx.getValueTable().getName().equals(name)) {
return true;
}
}
return false;
}
/**
* Returns the currently visible {@code HibernateValueTableTransaction} for the specified table name.
* <p/>
* Note that this method will throw an exception if no transaction exists for the table name. Use
* {@code #hasTableTransaction} to test the existence of a transaction before calling this method.
*
* @param name the name of the value table
* @return the instance of {@code HibernateValueTableTransaction} if one exists.
* @throws IllegalStateException if no such transaction instance exists.
*/
HibernateValueTableTransaction getTableTransaction(String name) {
for(HibernateValueTableTransaction tableTx : lookupTableTransactions()) {
if(tableTx.getValueTable().getName().equals(name)) {
return tableTx;
}
}
throw new IllegalStateException("No transaction exists on table " + name);
}
/**
* Creates a new {@code HibernateValueTableTransaction} for the specified value table. If a transaction already
* exists, that transaction is returned.
*
* @param valueTable the value table for which to create the transaction.
* @param createTableTransaction true when this transaction is creating the value table, false otherwise.
* @return a {@code HibernateValueTableTransaction} instance for the specified {@code HibernateValueTable}
*/
synchronized HibernateValueTableTransaction newTableTransaction(HibernateValueTable valueTable,
boolean createTableTransaction) {
if(hasTableTransaction(valueTable.getName())) {
return getTableTransaction(valueTable.getName());
}
HibernateValueTableTransaction tableTx = new HibernateValueTableTransaction(valueTable, createTableTransaction);
lookupTableTransactions().add(tableTx);
return tableTx;
}
/**
* Returns the list of {@code HibernateValueTableTransaction} associated with the current
* {@code org.hibernate.Transaction}. Within one Hibernate transaction, several value tables may be affected, as such,
* this method returns a list of {@code HibernateValueTableTransaction} instances. This method never returns null.
* <p/>
* If no Hibernate transaction exists this method returns an empty list.
*
* @return list of {@code HibernateValueTableTransaction} associated with the current
* {@code org.hibernate.Transaction}
*/
private synchronized Collection<HibernateValueTableTransaction> lookupTableTransactions() {
Session currentSession;
try {
currentSession = getSessionFactory().getCurrentSession();
} catch(HibernateException e) {
// No current session. Obviously, no transaction.
return Collections.emptyList();
}
autoCleanSyncMap();
Transaction tx = currentSession.getTransaction();
if(tx != null) {
List<HibernateValueTableTransaction> tableTxs = syncMap
.putIfAbsent(tx, new ArrayList<HibernateValueTableTransaction>());
if(tableTxs == null) {
tableTxs = syncMap.get(tx);
}
return tableTxs;
} else {
return Collections.emptyList();
}
}
private void autoCleanSyncMap() {
for (Map.Entry<Transaction, List<HibernateValueTableTransaction>> entry : syncMap.entrySet()) {
List<HibernateValueTableTransaction> txs = entry.getValue();
if (txs.size() > 0) {
List<HibernateValueTableTransaction> toRemove = Lists.newArrayList();
for (HibernateValueTableTransaction tx : txs) {
if (tx.isClosed()) toRemove.add(tx);
}
txs.removeAll(toRemove);
}
if (txs.isEmpty()) syncMap.remove(entry.getKey());
}
}
}