/*
* 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.views;
import java.util.*;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import org.obiba.magma.Datasource;
import org.obiba.magma.Disposable;
import org.obiba.magma.Initialisable;
import org.obiba.magma.MagmaCacheExtension;
import org.obiba.magma.MagmaEngine;
import org.obiba.magma.NoSuchValueSetException;
import org.obiba.magma.NoSuchVariableException;
import org.obiba.magma.Timestamps;
import org.obiba.magma.Value;
import org.obiba.magma.ValueSet;
import org.obiba.magma.ValueTable;
import org.obiba.magma.ValueType;
import org.obiba.magma.Variable;
import org.obiba.magma.VariableEntity;
import org.obiba.magma.VariableValueSource;
import org.obiba.magma.VariableValueSourceWrapper;
import org.obiba.magma.VectorSource;
import org.obiba.magma.support.AbstractValueTableWrapper;
import org.obiba.magma.support.AbstractVariableValueSourceWrapper;
import org.obiba.magma.support.Disposables;
import org.obiba.magma.support.Initialisables;
import org.obiba.magma.support.VariableEntitiesCache;
import org.obiba.magma.transform.BijectiveFunction;
import org.obiba.magma.transform.BijectiveFunctions;
import org.obiba.magma.transform.TransformingValueTable;
import org.obiba.magma.type.DateTimeType;
import org.obiba.magma.views.support.AllClause;
import org.obiba.magma.views.support.NoneClause;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@SuppressWarnings("OverlyCoupledClass")
public class View extends AbstractValueTableWrapper implements Initialisable, Disposable, TransformingValueTable {
private static final Logger log = LoggerFactory.getLogger(View.class);
private String name;
@NotNull
private ValueTable from;
@NotNull
private SelectClause select;
@NotNull
private WhereClause where;
/**
* A list of derived variables. Mutually exclusive with "select".
*/
@NotNull
private ListClause variables;
private Value created;
private Value updated;
// need to be transient because of XML serialization of Views
@Nullable
@SuppressWarnings("TransientFieldInNonSerializableClass")
private transient ViewAwareDatasource viewDatasource;
@Nullable
@SuppressWarnings("TransientFieldInNonSerializableClass")
private transient VariableEntitiesCache variableEntitiesCache;
/**
* No-arg constructor for XStream.
*/
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR",
justification = "Needed by XStream")
public View() {
setSelectClause(new AllClause());
setWhereClause(new AllClause());
setListClause(new NoneClause());
}
public View(String name, ValueTable... from) {
this(name, new AllClause(), new AllClause(), null, from);
}
public View(String name, String[] innerFrom, ValueTable... from) {
this(name, new AllClause(), new AllClause(), innerFrom, from);
}
@SuppressWarnings("ConstantConditions")
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR",
justification = "Needed by XStream")
public View(String name, @NotNull SelectClause selectClause, @NotNull WhereClause whereClause, String[] innerFrom,
@NotNull ValueTable... from) {
Preconditions.checkArgument(selectClause != null, "null selectClause");
Preconditions.checkArgument(whereClause != null, "null whereClause");
Preconditions.checkArgument(from != null && from.length > 0, "null or empty table list");
this.name = name;
this.from = from.length > 1 ?
innerFrom != null ?
new JoinTable(Arrays.<ValueTable>asList(from), Arrays.<String>asList(innerFrom))
: new JoinTable(Arrays.<ValueTable>asList(from))
: from[0];
setSelectClause(selectClause);
setWhereClause(whereClause);
setListClause(new NoneClause());
created = DateTimeType.get().now();
updated = DateTimeType.get().now();
}
@Override
public void initialise() {
getListClause().setValueTable(this);
Initialisables.initialise(getWrappedValueTable(), getSelectClause(), getWhereClause(), getListClause());
if(isViewOfDerivedVariables()) {
setSelectClause(new NoneClause());
} else if(!(getSelectClause() instanceof NoneClause)) {
setListClause(new NoneClause());
} else {
setListClause(new NoneClause());
setSelectClause(new AllClause());
}
}
@Override
public void dispose() {
Disposables.silentlyDispose(getWrappedValueTable(), getSelectClause(), getWhereClause(), getListClause());
}
public void setName(String name) {
this.name = name;
}
@NotNull
public SelectClause getSelectClause() {
return select;
}
@NotNull
public WhereClause getWhereClause() {
return where;
}
@NotNull
public ListClause getListClause() {
return variables;
}
/**
* Returns true is this is a {@link View} of derived variables, false if this is a {@code View} of selected (existing)
* variables.
*/
private boolean isViewOfDerivedVariables() {
return !(getListClause() instanceof NoneClause);
}
@NotNull
@Override
public Datasource getDatasource() {
return viewDatasource == null ? getWrappedValueTable().getDatasource() : viewDatasource;
}
@Override
@NotNull
public ValueTable getWrappedValueTable() {
return from;
}
@NotNull
@Override
public Timestamps getTimestamps() {
return new Timestamps() {
@NotNull
@Override
public Value getLastUpdate() {
if(updated == null || updated.isNull()) {
return getWrappedValueTable().getTimestamps().getLastUpdate();
}
Value fromUpdate = getWrappedValueTable().getTimestamps().getLastUpdate();
return !fromUpdate.isNull() && updated.compareTo(fromUpdate) < 0 ? fromUpdate : updated;
}
@NotNull
@Override
public Value getCreated() {
return created == null || created.isNull() ? getWrappedValueTable().getTimestamps().getCreated() : created;
}
};
}
@SuppressWarnings({ "AssignmentToMethodParameter", "PMD.AvoidReassigningParameters" })
public void setUpdated(@Nullable Value updated) {
if(updated == null) updated = DateTimeType.get().nullValue();
if(updated.getValueType() != DateTimeType.get()) throw new IllegalArgumentException();
this.updated = updated;
}
@SuppressWarnings({ "AssignmentToMethodParameter", "PMD.AvoidReassigningParameters" })
public void setCreated(@Nullable Value created) {
if(created == null) created = DateTimeType.get().nullValue();
if(created.getValueType() != DateTimeType.get()) throw new IllegalArgumentException();
this.created = created;
}
@Override
public boolean isView() {
return true;
}
@Override
public String getTableReference() {
return (viewDatasource == null || getDatasource() == null ? "null" : getDatasource().getName()) + "." + getName();
}
@Override
public int getVariableCount() {
return Iterables.size(getVariables());
}
@Override
public int getValueSetCount() {
return getVariableEntityCount();
}
@Override
public int getVariableEntityCount() {
return Iterables.size(getVariableEntities());
}
@Override
@SuppressWarnings("ChainOfInstanceofChecks")
public boolean hasValueSet(@Nullable VariableEntity entity) {
if(entity == null) return false;
return getVariableEntities().contains(entity);
}
@Override
public Iterable<ValueSet> getValueSets(Iterable<VariableEntity> entities) {
List<VariableEntity> unmappedEntities = Collections.synchronizedList(Lists.newArrayList());
StreamSupport.stream(entities.spliterator(), false) //
.forEach(entity -> unmappedEntities.add(getVariableEntityMappingFunction().unapply(entity)));
// do not use Guava functional stuff to avoid multiple iterations over valueSets
List<ValueSet> valueSets = Collections.synchronizedList(Lists.newArrayList());
StreamSupport.stream(super.getValueSets(unmappedEntities).spliterator(), false) //
.forEach(valueSet -> {
// replacing each ValueSet with one that points at the current View
valueSet = getValueSetMappingFunction().apply(valueSet);
// result of transformation might have returned a non-mappable entity
if(valueSet != null && valueSet.getVariableEntity() != null) {
valueSets.add(valueSet);
}
});
return valueSets;
}
@Override
public ValueSet getValueSet(VariableEntity entity) throws NoSuchValueSetException {
VariableEntity unmapped = getVariableEntityMappingFunction().unapply(entity);
if(unmapped == null) throw new NoSuchValueSetException(this, entity);
ValueSet valueSet = super.getValueSet(unmapped);
if(!getWhereClause().where(valueSet, this)) throw new NoSuchValueSetException(this, entity);
return getValueSetMappingFunction().apply(valueSet);
}
@Override
public Timestamps getValueSetTimestamps(VariableEntity entity) throws NoSuchValueSetException {
VariableEntity unmapped = getVariableEntityMappingFunction().unapply(entity);
if(unmapped == null) throw new NoSuchValueSetException(this, entity);
return super.getValueSetTimestamps(unmapped);
}
@Override
public Iterable<Timestamps> getValueSetTimestamps(SortedSet<VariableEntity> entities) {
List<Timestamps> timestamps = Lists.newArrayList();
for (VariableEntity entity : entities) {
timestamps.add(getValueSetTimestamps(entity));
}
return timestamps;
}
@Override
public Iterable<Variable> getVariables() {
if(from instanceof JoinTable) {
((JoinTable) from).analyseVariables();
}
return isViewOfDerivedVariables() ? getListVariables() : getSelectVariables();
}
private Iterable<Variable> getSelectVariables() {
return Iterables.filter(super.getVariables(), input -> getSelectClause().select(input));
}
private Iterable<Variable> getListVariables() {
Collection<Variable> listVariables = new LinkedHashSet<>();
for(VariableValueSource variableValueSource : getListClause().getVariableValueSources()) {
listVariables.add(variableValueSource.getVariable());
}
return listVariables;
}
@Override
public boolean hasVariable(@SuppressWarnings("ParameterHidesMemberVariable") String name) {
try {
getVariable(name);
return true;
} catch(NoSuchVariableException e) {
return false;
}
}
@Override
public Variable getVariable(String variableName) throws NoSuchVariableException {
return isViewOfDerivedVariables() //
? getListVariable(variableName) //
: getSelectVariable(variableName);
}
private Variable getSelectVariable(String variableName) throws NoSuchVariableException {
Variable variable = super.getVariable(variableName);
if(getSelectClause().select(variable)) {
return variable;
}
throw new NoSuchVariableException(variableName);
}
private Variable getListVariable(String variableName) throws NoSuchVariableException {
return getListClause().getVariableValueSource(variableName).getVariable();
}
@Override
public Value getValue(Variable variable, ValueSet valueSet) {
if(isViewOfDerivedVariables()) {
return getListClauseValue(variable, valueSet);
}
if(!getWhereClause().where(valueSet, this)) {
throw new NoSuchValueSetException(this, valueSet.getVariableEntity());
}
return super.getValue(variable, getValueSetMappingFunction().unapply(valueSet));
}
private Value getListClauseValue(Variable variable, ValueSet valueSet) {
return getListClauseVariableValueSource(variable.getName()).getValue(valueSet);
}
private VariableValueSource getListClauseVariableValueSource(String variableName) {
VariableValueSource variableValueSource = getListClause().getVariableValueSource(variableName);
return getVariableValueSourceMappingFunction().apply(variableValueSource);
}
@Override
public VariableValueSource getVariableValueSource(String variableName) throws NoSuchVariableException {
if(isViewOfDerivedVariables()) {
return getListClauseVariableValueSource(variableName);
}
// Call getVariable(variableName) to check the SelectClause (if there is one). If the specified variable
// is not selected by the SelectClause, this will result in a NoSuchVariableException.
getVariable(variableName);
// Variable "survived" the SelectClause. Go ahead and call the base class method.
return getVariableValueSourceMappingFunction().apply(super.getVariableValueSource(variableName));
}
@Override
public synchronized Set<VariableEntity> getVariableEntities() {
Value tableWrapperLastUpdate = getTimestamps().getLastUpdate();
VariableEntitiesCache eCache = getVariableEntitiesCache();
if(eCache == null || !eCache.isUpToDate(tableWrapperLastUpdate)) {
eCache = new VariableEntitiesCache(loadVariableEntities(), tableWrapperLastUpdate);
if(MagmaEngine.get().hasExtension(MagmaCacheExtension.class)) {
MagmaCacheExtension cacheExtension = MagmaEngine.get().getExtension(MagmaCacheExtension.class);
if(cacheExtension.hasVariableEntitiesCache()) {
cacheExtension.getVariableEntitiesCache().put(getTableCacheKey(), eCache);
} else {
variableEntitiesCache = eCache;
}
} else {
variableEntitiesCache = eCache;
}
}
return eCache.getEntities();
}
protected String getTableCacheKey() {
String key = getTableReference() + ";class=" + getClass().getName();
log.debug("tableCacheKey={}", key);
return key;
}
private VariableEntitiesCache getVariableEntitiesCache() {
if(MagmaEngine.get().hasExtension(MagmaCacheExtension.class)) {
MagmaCacheExtension cacheExtension = MagmaEngine.get().getExtension(MagmaCacheExtension.class);
if(!cacheExtension.hasVariableEntitiesCache()) return variableEntitiesCache;
Cache.ValueWrapper wrapper = cacheExtension.getVariableEntitiesCache().get(getTableCacheKey());
return wrapper == null ? variableEntitiesCache : (VariableEntitiesCache) wrapper.get();
} else {
return variableEntitiesCache;
}
}
protected Set<VariableEntity> loadVariableEntities() {
// do not use Guava functional stuff to avoid multiple iterations over entities
Set<VariableEntity> entities = Sets.newConcurrentHashSet();
if(hasVariables()) {
StreamSupport.stream(super.getVariableEntities().spliterator(), false) //
.forEach(entity -> {
// filter the resulting entities to remove the ones for which hasValueSet() is false
// (usually due to a where clause)
boolean hasValueSet = true;
if(getWhereClause() instanceof AllClause) {
hasValueSet = true;
} else if(getWhereClause() instanceof NoneClause) {
hasValueSet = false;
} else {
ValueSet valueSet = super.getValueSet(entity);
hasValueSet = getWhereClause().where(valueSet, this);
}
if(hasValueSet) {
VariableEntity mappedEntity = getVariableEntityMappingFunction().apply(entity);
entities.add(mappedEntity);
}
});
}
return entities;
}
public void setDatasource(ViewAwareDatasource datasource) {
viewDatasource = datasource;
}
@NotNull
@Override
public String getName() {
return name;
}
@SuppressWarnings("ConstantConditions")
public void setSelectClause(@NotNull SelectClause selectClause) {
Preconditions.checkArgument(selectClause != null, "null selectClause");
select = selectClause;
}
@SuppressWarnings("ConstantConditions")
public void setWhereClause(@NotNull WhereClause whereClause) {
Preconditions.checkArgument(whereClause != null, "null whereClause");
where = whereClause;
}
@SuppressWarnings("ConstantConditions")
public void setListClause(@NotNull ListClause listClause) {
Preconditions.checkArgument(listClause != null, "null listClause");
variables = listClause;
}
@NotNull
@Override
public BijectiveFunction<VariableEntity, VariableEntity> getVariableEntityMappingFunction() {
return BijectiveFunctions.identity();
}
@NotNull
@Override
public BijectiveFunction<ValueSet, ValueSet> getValueSetMappingFunction() {
return new BijectiveFunction<ValueSet, ValueSet>() {
@Override
public ValueSet unapply(@SuppressWarnings("ParameterHidesMemberVariable") ValueSet from) {
return ((ValueSetWrapper) from).getWrappedValueSet();
}
@Override
public ValueSet apply(@SuppressWarnings("ParameterHidesMemberVariable") ValueSet from) {
return new ValueSetWrapper(View.this, from);
}
};
}
@NotNull
@Override
public BijectiveFunction<VariableValueSource, VariableValueSource> getVariableValueSourceMappingFunction() {
return new BijectiveFunction<VariableValueSource, VariableValueSource>() {
@Override
public VariableValueSource apply(@SuppressWarnings("ParameterHidesMemberVariable") VariableValueSource from) {
return new ViewVariableValueSource(from);
}
@Override
public VariableValueSource unapply(@SuppressWarnings("ParameterHidesMemberVariable") VariableValueSource from) {
return ((VariableValueSourceWrapper) from).getWrapped();
}
};
}
private boolean hasVariables() {
return !(select instanceof NoneClause) || variables.getVariableValueSources().iterator().hasNext();
}
protected class ViewVariableValueSource extends AbstractVariableValueSourceWrapper {
public ViewVariableValueSource(VariableValueSource wrapped) {
super(wrapped);
}
@NotNull
@Override
public Value getValue(ValueSet valueSet) {
return getWrapped().getValue(getValueSetMappingFunction().unapply(valueSet));
}
@NotNull
@Override
public VectorSource asVectorSource() {
return new ViewVectorSource(super.asVectorSource());
}
private class ViewVectorSource implements VectorSource {
private final VectorSource wrapped;
private SortedSet<VariableEntity> mappedEntities;
private ViewVectorSource(VectorSource wrapped) {
this.wrapped = wrapped;
}
@Override
public ValueType getValueType() {
return wrapped.getValueType();
}
@Override
public Iterable<Value> getValues(SortedSet<VariableEntity> entities) {
return wrapped.getValues(getMappedEntities(entities));
}
private SortedSet<VariableEntity> getMappedEntities(Iterable<VariableEntity> entities) {
if(mappedEntities == null) {
mappedEntities = Collections.synchronizedSortedSet(Sets.newTreeSet());
StreamSupport.stream(entities.spliterator(), false) // not parallel to preserve entities order
.forEach(entity -> mappedEntities.add(getVariableEntityMappingFunction().unapply(entity)));
}
return mappedEntities;
}
@Override
@SuppressWarnings("SimplifiableIfStatement")
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null || getClass() != obj.getClass()) return false;
return wrapped.equals(((ViewVectorSource) obj).wrapped);
}
@Override
public int hashCode() {
return wrapped.hashCode();
}
}
}
//
// Builder
//
@SuppressWarnings({ "UnusedDeclaration", "StaticMethodOnlyUsedInOneClass" })
public static class Builder {
private final String name;
private final ValueTable[] from;
private String[] innerFrom;
private SelectClause selectClause;
private WhereClause whereClause;
private ListClause listClause;
public Builder(String name, @NotNull ValueTable... from) {
this.name = name;
this.from = from;
}
public static Builder newView(String name, @NotNull ValueTable... from) {
return new Builder(name, from);
}
public static Builder newView(String name, @NotNull List<ValueTable> from) {
return new Builder(name, from.toArray(new ValueTable[from.size()]));
}
public Builder select(@NotNull SelectClause selectClause) {
this.selectClause = selectClause;
return this;
}
public Builder innerFrom(String... tableReferences) {
this.innerFrom = tableReferences;
return this;
}
public Builder innerFrom(List<String> tableReferences) {
this.innerFrom = tableReferences.toArray(new String[tableReferences.size()]);
return this;
}
public Builder where(@NotNull WhereClause whereClause) {
this.whereClause = whereClause;
return this;
}
public Builder cacheWhere() {
whereClause = new CachingWhereClause(whereClause);
return this;
}
public View build() {
View view = new View(name, innerFrom,from);
if (selectClause != null) view.setSelectClause(selectClause);
if (listClause != null) view.setListClause(listClause);
if (whereClause != null) view.setWhereClause(whereClause);
return view;
}
public Builder list(ListClause listClause) {
this.listClause = listClause;
return this;
}
}
}