/* * 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.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedSet; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import org.hibernate.Query; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.Session; import org.hibernate.criterion.Projections; 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.*; import org.obiba.magma.datasource.hibernate.converter.HibernateMarshallingContext; import org.obiba.magma.datasource.hibernate.domain.Timestamped; import org.obiba.magma.datasource.hibernate.domain.ValueSetState; import org.obiba.magma.datasource.hibernate.domain.ValueTableState; import org.obiba.magma.datasource.hibernate.domain.VariableState; import org.obiba.magma.support.AbstractValueTable; import org.obiba.magma.support.AbstractVariableEntityProvider; import org.obiba.magma.support.NullTimestamps; import org.obiba.magma.support.VariableEntityBean; import org.obiba.magma.type.DateTimeType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; @SuppressWarnings("OverlyCoupledClass") class HibernateValueTable extends AbstractValueTable { private static final Logger log = LoggerFactory.getLogger(HibernateValueTable.class); private final Serializable valueTableId; private final HibernateVariableEntityProvider variableEntityProvider; private Map<String, Timestamps> valueSetTimestamps; HibernateValueTable(Datasource datasource, ValueTableState state) { super(datasource, state.getName()); valueTableId = state.getId(); setVariableEntityProvider(variableEntityProvider = new HibernateVariableEntityProvider(state.getEntityType())); } @Override public void initialise() { super.initialise(); variableEntityProvider.initialise(); readVariables(); } @NotNull @Override public HibernateDatasource getDatasource() { return (HibernateDatasource) super.getDatasource(); } @Override public ValueSet getValueSet(VariableEntity entity) throws NoSuchValueSetException { if(!hasValueSet(entity)) { throw new NoSuchValueSetException(this, entity); } return new HibernateValueSet(this, entity); } @Override protected ValueSetBatch getValueSetsBatch(List<VariableEntity> entities) { return new HibernateValueSetBatch(this, entities); } @Override public boolean canDropValueSets() { return true; } @Override public void dropValueSets() { Session session = getDatasource().getSessionFactory().getCurrentSession(); getDatasource().deleteValueSets(getDatasource().getName() + "." + getName(), session, session.getNamedQuery("findValueSetIdsByTableId").setParameter("valueTableId", getValueTableState().getId()) .list()); getValueTableState().setUpdated(new Date()); variableEntityProvider.initialise(); } @Override public Timestamps getValueSetTimestamps(VariableEntity entity) throws NoSuchValueSetException { if(valueSetTimestamps == null) { cacheValueSetTimestamps(); } Timestamps cachedTimestamps = valueSetTimestamps.get(entity.getIdentifier()); if(cachedTimestamps == null) { if(hasValueSet(entity)) { // not in the cache because cache is outdated cacheValueSetTimestamps(); cachedTimestamps = valueSetTimestamps.get(entity.getIdentifier()); } else { // not in the cache because not a valid value set throw new NoSuchValueSetException(this, entity); } } return cachedTimestamps == null ? NullTimestamps.get() : cachedTimestamps; } @Override public Iterable<Timestamps> getValueSetTimestamps(final SortedSet<VariableEntity> entities) { if(entities.isEmpty()) { return ImmutableList.of(); } return () -> new TimestampsIterator(entities.iterator()); } void dropValueSet(VariableEntity entity, Serializable valueSetId) { Session session = getDatasource().getSessionFactory().getCurrentSession(); getDatasource() .deleteValueSets(getDatasource().getName() + "." + getName(), session, Collections.singleton(valueSetId)); getValueTableState().setUpdated(new Date()); variableEntityProvider.remove(entity); } @SuppressWarnings("unchecked") private void cacheValueSetTimestamps() { valueSetTimestamps = Maps.newHashMap(); Query query = getDatasource().getSessionFactory().getCurrentSession().createSQLQuery( "SELECT ve.identifier, vs.created, vs.updated FROM value_set vs, variable_entity ve " + "WHERE ve.id = vs.variable_entity_id AND vs.value_table_id = :value_table_id AND ve.type = :entity_type").setParameter( "value_table_id", valueTableId) // .setParameter("entity_type", getEntityType()); for(final Object[] row : (List<Object[]>) query.list()) { valueSetTimestamps.put((String) row[0], new Timestamps() { @NotNull @Override public Value getCreated() { return DateTimeType.get().valueOf(row[1]); } @NotNull @Override public Value getLastUpdate() { return DateTimeType.get().valueOf(row[2]); } }); } } @NotNull @Override public Timestamps getTimestamps() { return createTimestamps(getValueTableState()); } @Override public int getVariableCount() { return ((Number) getDatasource().getSessionFactory().getCurrentSession() // .createCriteria(VariableState.class) // .setProjection(Projections.rowCount()) // .add(Restrictions.eq("valueTable", getValueTableState())) // .uniqueResult()).intValue(); } @Override public int getValueSetCount() { return ((Number) getDatasource().getSessionFactory().getCurrentSession() // .createCriteria(ValueSetState.class) // .setProjection(Projections.rowCount()) // .add(Restrictions.eq("valueTable", getValueTableState())) // .uniqueResult()).intValue(); } @Override public int getVariableEntityCount() { return getValueSetCount(); } public void setName(String name) { ValueTableState tableState = getValueTableState(); tableState.setName(name); tableState.setUpdated(new Date()); getDatasource().getSessionFactory().getCurrentSession().save(tableState); this.name = name; } static Timestamps createTimestamps(@Nullable final Timestamped timestamped) { return timestamped == null ? NullTimestamps.get() : new Timestamps() { @NotNull @Override public Value getLastUpdate() { return DateTimeType.get().valueOf(timestamped.getUpdated()); } @NotNull @Override public Value getCreated() { return DateTimeType.get().valueOf(timestamped.getCreated()); } }; } @Override public VariableValueSource getVariableValueSource(final String variableName) throws NoSuchVariableException { try { return Iterables.find(getSources(), new Predicate<VariableValueSource>() { @Override public boolean apply(VariableValueSource variableValueSource) { return variableValueSource.getVariable().getName().equals(variableName); } }); } catch(NoSuchElementException e) { throw new NoSuchVariableException(getName(), variableName); } } @Override public boolean hasVariable(String variableName) { for(VariableValueSource source : getSources()) { if(source.getVariable().getName().equals(variableName)) { return true; } } return false; } /** * Overridden to include uncommitted sources when a transaction exists on this table and is visible in the current * session. */ @Override protected Set<VariableValueSource> getSources() { if(getDatasource().hasTableTransaction(getName())) { return new ImmutableSet.Builder<VariableValueSource>() // .addAll(super.getSources()) // .addAll(getDatasource().getTableTransaction(getName()).getUncommittedSources()) // .build(); } return Collections.unmodifiableSet(super.getSources()); } ValueTableState getValueTableState() { return (ValueTableState) getDatasource().getSessionFactory().getCurrentSession() .get(ValueTableState.class, valueTableId); } HibernateMarshallingContext createContext() { return getDatasource().createContext(getValueTableState()); } void commitEntities(Collection<VariableEntity> newEntities) { variableEntityProvider.entities.addAll(newEntities); } void commitSources(Collection<VariableValueSource> uncommittedSources) { addVariableValueSources(uncommittedSources); } void commitRemovedSources(Iterable<VariableValueSource> uncommittedRemovedSources) { removeVariableValueSources(uncommittedRemovedSources); } VariableState getVariableState(Variable variable) { HibernateVariableValueSource variableValueSource = (HibernateVariableValueSource) getVariableValueSource( variable.getName()); return variableValueSource.getVariableState(); } private void readVariables() { log.debug("Populating variable cache for table {}", getName()); VariableValueSourceFactory factory = new HibernateVariableValueSourceFactory(this); addVariableValueSources(factory.createSources()); log.debug("Populating variable cache - done. {} variables loaded", super.getSources().size()); } Serializable getValueTableId() { return valueTableId; } void refreshEntityProvider() { ((HibernateVariableEntityProvider)getVariableEntityProvider()).initialise(); } public class HibernateVariableEntityProvider extends AbstractVariableEntityProvider implements Initialisable { private final Set<VariableEntity> entities = new LinkedHashSet<>(); public HibernateVariableEntityProvider(String entityType) { super(entityType); } @Override public void initialise() { log.debug("Populating entity cache for table {}", getName()); entities.clear(); // get the variable entities that have a value set in the table AssociationCriteria criteria = AssociationCriteria .create(ValueSetState.class, getDatasource().getSessionFactory().getCurrentSession()) .add("valueTable.id", Operation.eq, valueTableId); for(Object obj : criteria.list()) { VariableEntity entity = ((ValueSetState) obj).getVariableEntity(); entities.add(new VariableEntityBean(entity.getType(), entity.getIdentifier())); } log.debug("Populating entity cache - done. {} entities loaded.", entities.size()); } /** * Returns the set of entities in this table. Will also include uncommitted entities when a transaction is active * for this table in the current session. */ @NotNull @Override public Set<VariableEntity> getVariableEntities() { //TODO cache these entities instead of recreating an ImmutableSet each time if(getDatasource().hasTableTransaction(getName())) { return ImmutableSet.copyOf( Iterables.concat(entities, getDatasource().getTableTransaction(getName()).getUncommittedEntities())); } return Collections.unmodifiableSet(entities); } public void remove(VariableEntity entity) { entities.remove(entity); } } private class TimestampsIterator implements Iterator<Timestamps> { private final ScrollableResults results; private boolean hasNextResults; private boolean closed; private final Iterator<VariableEntity> entities; private final Map<String, Timestamps> timestampsMap = Maps.newHashMap(); private TimestampsIterator(Iterator<VariableEntity> entities) { this.entities = entities; Query query = getCurrentSession().getNamedQuery("findValueSetTimestampsByTableId") // .setParameter("valueTableId", getValueTableState().getId()); results = query.scroll(ScrollMode.FORWARD_ONLY); hasNextResults = results.next(); } private Session getCurrentSession() { return getDatasource().getSessionFactory().getCurrentSession(); } @Override public boolean hasNext() { return entities.hasNext(); } @Override public Timestamps next() { VariableEntity entity = entities.next(); if(timestampsMap.containsKey(entity.getIdentifier())) return getTimestampsFromMap(entity); boolean found = false; // Scroll until we find the required entity or reach the end of the results while(hasNextResults && !found) { String id = results.getString(0); Value created = DateTimeType.get().valueOf(results.getDate(1)); Value updated = DateTimeType.get().valueOf(results.getDate(2)); timestampsMap.put(id, new TimestampsBean(created, updated)); if(entity.getIdentifier().equals(id)) { found = true; } hasNextResults = results.next(); } closeCursorIfNecessary(); if(timestampsMap.containsKey(entity.getIdentifier())) return getTimestampsFromMap(entity); return NullTimestamps.get(); } @Override public void remove() { throw new UnsupportedOperationException(); } /** * No duplicate of entities, so remove value from map once get. * * @param entity * @return */ private Timestamps getTimestampsFromMap(VariableEntity entity) { Timestamps value = timestampsMap.get(entity.getIdentifier()); timestampsMap.remove(entity.getIdentifier()); return value; } private void closeCursorIfNecessary() { if(!closed) { // Close the cursor if we don't have any more results or no more entities to return if(!hasNextResults || !hasNext()) { closed = true; results.close(); } } } } }