/*
* 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.util.Date;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import org.hibernate.FlushMode;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.json.JSONException;
import org.json.JSONObject;
import org.obiba.core.domain.IEntity;
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.NoSuchVariableException;
import org.obiba.magma.Value;
import org.obiba.magma.ValueTableWriter;
import org.obiba.magma.Variable;
import org.obiba.magma.VariableEntity;
import org.obiba.magma.datasource.hibernate.converter.HibernateMarshallingContext;
import org.obiba.magma.datasource.hibernate.converter.VariableConverter;
import org.obiba.magma.datasource.hibernate.converter.VariableEntityConverter;
import org.obiba.magma.datasource.hibernate.domain.ValueSetBinaryValue;
import org.obiba.magma.datasource.hibernate.domain.ValueSetState;
import org.obiba.magma.datasource.hibernate.domain.ValueSetValue;
import org.obiba.magma.datasource.hibernate.domain.VariableEntityState;
import org.obiba.magma.datasource.hibernate.domain.VariableState;
import org.obiba.magma.type.BinaryType;
import org.obiba.magma.type.TextType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import static com.google.common.base.Preconditions.checkArgument;
class HibernateValueTableWriter implements ValueTableWriter {
private static final Logger log = LoggerFactory.getLogger(HibernateValueTableWriter.class);
private final HibernateValueTable valueTable;
private final HibernateValueTableTransaction transaction;
private final VariableConverter variableConverter = VariableConverter.getInstance();
private final Session session;
private final HibernateVariableValueSourceFactory valueSourceFactory;
private boolean errorOccurred = false;
private boolean dirty = false;
private final HibernateMarshallingContext context;
HibernateValueTableWriter(HibernateValueTableTransaction transaction) {
if(transaction == null) throw new IllegalArgumentException("transaction cannot be null");
this.transaction = transaction;
valueTable = transaction.getValueTable();
session = valueTable.getDatasource().getSessionFactory().getCurrentSession();
valueSourceFactory = new HibernateVariableValueSourceFactory(valueTable);
if(session.getFlushMode() != FlushMode.MANUAL) {
session.setFlushMode(FlushMode.MANUAL);
}
context = valueTable.createContext();
}
@NotNull
@Override
public ValueSetWriter writeValueSet(@NotNull VariableEntity entity) {
return new HibernateValueSetWriter(entity);
}
@Override
public VariableWriter writeVariables() {
return new HibernateVariableWriter();
}
@Override
public void close() {
}
private void updateTableLastUpdate() {
valueTable.getValueTableState().setUpdated(new Date());
}
private class HibernateVariableWriter implements VariableWriter {
private HibernateVariableWriter() {
}
@Override
public void writeVariable(@NotNull Variable variable) {
//noinspection ConstantConditions
checkArgument(variable != null, "variable cannot be null");
checkArgument(valueTable.isForEntityType(variable.getEntityType()),
"Wrong entity type for variable '" + variable.getName() + "': " + valueTable.getEntityType() +
" expected, " + variable.getEntityType() + " received.");
// add or update variable
errorOccurred = true;
VariableState state = variableConverter.marshal(variable, context);
transaction.addSource(valueSourceFactory.createSource(state));
errorOccurred = false;
dirty = true;
}
@Override
public void removeVariable(@NotNull Variable variable) {
errorOccurred = true;
VariableState variableState = valueTable.getVariableState(variable);
deleteVariableValues(variable, variableState);
transaction.removeSource(valueSourceFactory.createSource(variableState));
// I don't know why variable delete does not work but I had to manually delete categories (with attributes)
// and variable (with attributes) using native SQL and HQL...
deleteVariableCategories(variableState);
deleteVariable(variableState);
if (valueTable.getVariableCount() == 0) {
valueTable.refreshEntityProvider();
}
errorOccurred = false;
dirty = true;
}
private void deleteVariableValues(Variable variable, IEntity variableState) {
int nbDeletedValues = session.getNamedQuery("deleteVariableValueSetValues") //
.setParameter("variableId", variableState.getId()) //
.executeUpdate();
log.debug("Deleted {} value from {}", nbDeletedValues, valueTable.getName());
if(variable.getValueType().equals(BinaryType.get())) {
List<?> valueSetIds = session.getNamedQuery("findValueSetIdsByTableId") //
.setParameter("valueTableId", valueTable.getValueTableId()) //
.list();
if (valueSetIds != null && !valueSetIds.isEmpty()) {
int nbBinariesDeleted = session.getNamedQuery("deleteVariableBinaryValues") //
.setParameterList("valueSetIds", valueSetIds) //
.setParameter("variableId", variableState.getId()) //
.executeUpdate();
log.debug("Deleted {} binaries from {}", nbBinariesDeleted, valueTable.getName());
}
}
// delete empty value sets
int nbEmptyValueSets = session.getNamedQuery("deleteEmptyValueSets").executeUpdate();
log.debug("Deleted {} empty value sets from {}", nbEmptyValueSets, valueTable.getName());
// update all value sets last update
int updated = session.getNamedQuery("setLastUpdateForTableId") //
.setParameter("updated", new Date()) //
.setParameter("valueTableId", valueTable.getValueTableId()) //
.executeUpdate();
log.debug("Updated lastUpdate for {} value sets for {}", updated, valueTable.getName());
}
private void deleteVariableCategories(IEntity variableState) {
session.createSQLQuery("delete from category_attributes where category_id in " + //
"(select c.id from category c where c.variable_id = " + variableState.getId() + ")") //
.executeUpdate();
int deletedCategories = session.createQuery("delete CategoryState where variable = :variable")
.setEntity("variable", variableState).executeUpdate();
log.debug("Deleted {} categories", deletedCategories);
}
private void deleteVariable(VariableState variableState) {
session.createSQLQuery("delete from variable_attributes where variable_id = " + variableState.getId())
.executeUpdate();
int deletedVariables = session.createQuery("delete VariableState where valueTable = :table and name = :name")
.setEntity("table", variableState.getValueTable()).setString("name", variableState.getName()).executeUpdate();
log.debug("Deleted {} variables", deletedVariables);
}
@Override
public void close() {
if(!errorOccurred) {
if(dirty) {
updateTableLastUpdate();
dirty = false;
}
// persists data and empty the Session so we don't fill it up
session.flush();
session.clear();
}
}
}
private class HibernateValueSetWriter implements ValueSetWriter {
@SuppressWarnings("TypeMayBeWeakened")
private final VariableEntityConverter entityConverter = new VariableEntityConverter();
private final ValueSetState valueSetState;
@NotNull
private final VariableEntity entity;
private final boolean isNewValueSet;
private final Map<String, ValueSetValue> values;
private HibernateValueSetWriter(@NotNull VariableEntity entity) {
//noinspection ConstantConditions
if(entity == null) throw new IllegalArgumentException("entity cannot be null");
this.entity = entity;
// find entity or create it
VariableEntityState variableEntityState = entityConverter.marshal(entity, context);
// Will update version timestamp if it exists
ValueSetState state = (ValueSetState) AssociationCriteria.create(ValueSetState.class, session) //
.add("valueTable", Operation.eq, valueTable.getValueTableState()) //
.add("variableEntity", Operation.eq, variableEntityState) //
.getCriteria() //
.uniqueResult();
if(state == null) {
state = new ValueSetState(valueTable.getValueTableState(), variableEntityState);
// Persists the ValueSet
session.save(state);
session.refresh(state); //OPAL-2635
values = Maps.newHashMap();
isNewValueSet = true;
} else {
values = Maps.newHashMap(state.getValueMap());
isNewValueSet = false;
}
session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT).setScope(false)).lock(state);
valueSetState = state;
}
@Override
@SuppressWarnings("PMD.NcssMethodCount")
public void writeValue(@NotNull Variable variable, @NotNull Value value) {
//noinspection ConstantConditions
if(variable == null) throw new IllegalArgumentException("variable cannot be null");
//noinspection ConstantConditions
if(value == null) throw new IllegalArgumentException("value cannot be null");
try {
VariableState variableState = variableConverter.getStateForVariable(variable, valueTable.createContext());
if(variableState == null) {
throw new NoSuchVariableException(valueTable.getName(), variable.getName());
}
ValueSetValue valueSetValue = values.get(variable.getName());
if(valueSetValue == null) {
createValue(variable, value, variableState);
} else {
updateValue(variable, value, valueSetValue);
}
dirty = true;
} catch(RuntimeException | Error e) {
errorOccurred = true;
throw e;
}
}
@Override
public void remove() {
valueTable.dropValueSet(entity, valueSetState.getId());
}
private void createValue(Variable variable, Value value, VariableState variableState) {
if(value.isNull()) return;
ValueSetValue valueSetValue = new ValueSetValue(variableState, valueSetState);
if(BinaryType.get().equals(value.getValueType())) {
writeBinaryValue(valueSetValue, value, false);
} else {
valueSetValue.setValue(value);
}
addValue(variable, valueSetValue);
}
private void updateValue(Variable variable, Value value, ValueSetValue valueSetValue) {
if(value.isNull()) {
removeValue(variable, valueSetValue);
} else {
if(BinaryType.get().equals(value.getValueType())) {
writeBinaryValue(valueSetValue, value, true);
} else {
valueSetValue.setValue(value);
}
}
}
private void addValue(Variable variable, ValueSetValue valueSetValue) {
valueSetState.getValues().add(valueSetValue);
values.put(variable.getName(), valueSetValue);
}
private void removeValue(Variable variable, ValueSetValue valueSetValue) {
valueSetState.getValues().remove(valueSetValue);
values.remove(variable.getName());
}
@SuppressWarnings("ConstantConditions")
private void writeBinaryValue(ValueSetValue valueSetValue, Value value, boolean isUpdate) {
if(value.isSequence()) {
List<Value> sequenceValues = Lists.newArrayList();
if(!value.isNull()) {
int occurrence = 0;
for(Value valueOccurrence : value.asSequence().getValue()) {
sequenceValues.add(createBinaryValue(valueSetValue, valueOccurrence, occurrence++, isUpdate));
}
}
valueSetValue.setValue(TextType.get().sequenceOf(sequenceValues));
} else {
valueSetValue.setValue(createBinaryValue(valueSetValue, value, 0, isUpdate));
}
}
private Value createBinaryValue(ValueSetValue valueSetValue, Value inputValue, int occurrence, boolean isUpdate) {
ValueSetBinaryValue binaryValue = isUpdate ? findBinaryValue(valueSetValue, occurrence) : null;
if(binaryValue == null) {
binaryValue = createBinaryValue(valueSetValue, inputValue, occurrence);
} else if(inputValue.isNull()) {
session.delete(binaryValue);
} else {
// update
binaryValue.setValue((byte[]) inputValue.getValue());
}
if(binaryValue == null) {
// can be null if empty byte[]
return TextType.get().nullValue();
}
session.save(binaryValue);
return getBinaryMetadata(binaryValue);
}
private ValueSetBinaryValue findBinaryValue(ValueSetValue valueSetValue, int occurrence) {
return (ValueSetBinaryValue) session.getNamedQuery("findBinaryByValueSetValueAndOccurrence") //
.setParameter("variableId", valueSetValue.getVariable().getId()) //
.setParameter("valueSetId", valueSetValue.getValueSet().getId()) //
.setParameter("occurrence", occurrence) //
.uniqueResult();
}
@Nullable
private ValueSetBinaryValue createBinaryValue(ValueSetValue valueSetValue, Value value, int occurrence) {
if(value.isNull()) return null;
ValueSetBinaryValue binaryValue = new ValueSetBinaryValue(valueSetValue, occurrence);
binaryValue.setValue((byte[]) value.getValue());
return binaryValue;
}
private Value getBinaryMetadata(ValueSetBinaryValue binaryValue) {
try {
JSONObject properties = new JSONObject();
properties.put("size", binaryValue == null ? 0 : binaryValue.getSize());
return TextType.get().valueOf(properties.toString());
} catch(JSONException e) {
throw new MagmaRuntimeException(e);
}
}
@Override
public void close() {
if(!errorOccurred) {
if(isNewValueSet) {
// Make the entity visible within this transaction
transaction.addEntity(entity);
}
if(dirty) {
updateTableLastUpdate();
dirty = false;
}
// persists valueSetState and empty the Session so we don't fill it up
session.flush();
session.clear();
}
}
}
}