/* * 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 com.google.common.collect.*; import org.obiba.magma.*; import org.obiba.magma.support.UnionTimestamps; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import java.util.*; import java.util.stream.Collectors; @SuppressWarnings({"UnusedDeclaration", "TransientFieldInNonSerializableClass"}) public class JoinTable implements ValueTable, Initialisable { private static final int DEFAULT_ENTITY_COUNT = 5000; @NotNull private final List<ValueTable> tables; private final List<String> innerTableReferences; /** * Cached set of all variables of all tables in the join (i.e., the union). */ private transient Set<Variable> unionOfVariables; /** * Cached map of variable names to tables. */ @NotNull private transient final Multimap<Variable, ValueTable> variableTables = ArrayListMultimap.create(); /** * Map of variable value sources. */ @NotNull private transient final Map<String, JoinVariableValueSource> variableValueSourceMap = Maps.newHashMap(); /** * Map first found JoinableVariable by its name */ @NotNull private transient final Map<String, Variable> joinableVariablesByName = Maps.newHashMap(); // An arbitrary number to initialise the LinkedHashSet with a capacity close to the actual value // See getVariableEntities() private transient int lastEntityCount = DEFAULT_ENTITY_COUNT; private transient boolean variableAnalysed = false; /** * No-arg constructor (mainly for XStream). */ public JoinTable() { this(new ArrayList<ValueTable>()); } public JoinTable(@NotNull List<ValueTable> tables) { this(tables, true); } @SuppressWarnings("ConstantConditions") public JoinTable(@NotNull List<ValueTable> tables, boolean validateEntityTypes) { this(tables, null, validateEntityTypes); } public JoinTable(@NotNull List<ValueTable> tables, @Nullable List<String> innerTableReferences) { this(tables, innerTableReferences, true); } public JoinTable(@NotNull List<ValueTable> tables, @Nullable List<String> innerTableReferences, boolean validateEntityTypes) { if (tables == null) throw new IllegalArgumentException("null tables"); if (tables.isEmpty()) throw new IllegalArgumentException("empty tables"); if (validateEntityTypes) { String entityType = tables.get(0).getEntityType(); for (int i = 1; i < tables.size(); i++) { if (!tables.get(i).isForEntityType(entityType)) { throw new IllegalArgumentException("tables must all have the same entity type"); } } } this.tables = ImmutableList.copyOf(tables); this.innerTableReferences = innerTableReferences == null ? Lists.newArrayList() : innerTableReferences; } @NotNull public Map<String, Variable> getJoinableVariablesByName() { if (!variableAnalysed) analyseVariables(); return joinableVariablesByName; } public synchronized void analyseVariables() { if (variableAnalysed) return; tables.forEach(table -> table.getVariables().forEach(variable -> { Variable joinableVariable = VariableBean.Builder.newVariable(variable.getName(), variable.getValueType(), variable.getEntityType()) // .repeatable(variable.isRepeatable()).occurrenceGroup(variable.getOccurrenceGroup()) // .referencedEntityType(variable.getReferencedEntityType()) // .mimeType(variable.getMimeType()) // .unit(variable.getUnit()) // .build(); Variable existing = joinableVariablesByName.get(variable.getName()); if (existing != null && !existing.equals(joinableVariable)) { throw new IllegalArgumentException( "Cannot have variables with same name and different value type or repeatability: '" + buildJoinTableName() + "." + variable.getName() + "'" ); } variableTables.put(joinableVariable, table); joinableVariablesByName.put(variable.getName(), joinableVariable); }) ); variableAnalysed = true; } @NotNull public List<ValueTable> getTables() { // don't analyse variables here as it is called very often return tables; } @NotNull public List<String> getInnerTableReferences() { return innerTableReferences; } @NotNull @Override @SuppressWarnings({"NullableProblems", "ConstantConditions"}) @edu.umd.cs.findbugs.annotations.SuppressWarnings("NP_NONNULL_RETURN_VIOLATION") public Datasource getDatasource() { // A JoinTable does not belong to a Datasource (or does it? which one?). return null; } @Override public String getEntityType() { return getTables().get(0).getEntityType(); } @NotNull @Override public String getName() { return buildJoinTableName(); } @Override public Value getValue(Variable variable, ValueSet valueSet) { return getVariableValueSource(variable.getName()).getValue(valueSet); } @Override public ValueSet getValueSet(VariableEntity entity) throws NoSuchValueSetException { if (hasValueSet(entity)) { return new JoinValueSet(this, entity); } throw new NoSuchValueSetException(this, entity); } @Override public boolean canDropValueSets() { for (ValueTable table : tables) { if (!table.canDropValueSets()) return false; } return true; } @Override public void dropValueSets() { tables.forEach(ValueTable::dropValueSets); } @Override public Timestamps getValueSetTimestamps(VariableEntity entity) throws NoSuchValueSetException { return getValueSet(entity).getTimestamps(); } @Override public Iterable<Timestamps> getValueSetTimestamps(final SortedSet<VariableEntity> entities) { return () -> new JoinTimestampsIterator(JoinTable.this, entities); } @Override public Set<VariableEntity> getVariableEntities() { if (!variableAnalysed) analyseVariables(); // Set the initial capacity to the number of entities we saw in the previous call to this method Set<VariableEntity> entities = Collections.synchronizedSet(Sets.newLinkedHashSetWithExpectedSize(lastEntityCount)); getOuterTables().forEach(table -> entities.addAll(table.getVariableEntities())); // Remember this value so that next time around, the set is initialised with a capacity closer to the actual value. lastEntityCount = entities.size(); return entities; } @Override public Iterable<ValueSet> getValueSets() { return getValueSets(getVariableEntities()); } @Override public Iterable<ValueSet> getValueSets(Iterable<VariableEntity> entities) { return () -> new ValueSetIterator(entities); } @Override public boolean hasVariable(String name) { return getJoinableVariablesByName().containsKey(name); } @Override public Variable getVariable(String name) throws NoSuchVariableException { for (Variable variable : getVariables()) { if (variable.getName().equals(name)) { return variable; } } throw new NoSuchVariableException(name); } @Override public VariableValueSource getVariableValueSource(String variableName) throws NoSuchVariableException { if (!variableAnalysed) analyseVariables(); if (!variableValueSourceMap.containsKey(variableName)) { // find first variable with this name Variable joinableVariable = getJoinableVariablesByName().get(variableName); if (joinableVariable == null) { throw new NoSuchVariableException(variableName); } List<ValueTable> tablesWithVariable = getTablesWithVariable(joinableVariable); ValueTable table = Iterables.getFirst(tablesWithVariable, null); if (table == null) { throw new NoSuchVariableException(variableName); } variableValueSourceMap.put(variableName, new JoinVariableValueSource(joinableVariable, tablesWithVariable)); } return variableValueSourceMap.get(variableName); } @Override public Iterable<Variable> getVariables() { if (!variableAnalysed) analyseVariables(); return unionOfVariables(); } @Override public boolean hasValueSet(VariableEntity entity) { if (!variableAnalysed) analyseVariables(); for (ValueTable table : getOuterTables()) { if (table.hasValueSet(entity)) { return true; } } return false; } @Override public boolean isForEntityType(String entityType) { return getEntityType().equals(entityType); } @Override public void initialise() { for (ValueTable table : tables) { if (table instanceof Initialisable) { ((Initialisable) table).initialise(); } } } @NotNull @Override public Timestamps getTimestamps() { return new UnionTimestamps(getTables()); } @Override public boolean isView() { return false; } @Override public String getTableReference() { // A JoinTable does not belong to a Datasource (or does it? which one?). return ""; } @Override public int getVariableCount() { return Iterables.size(getVariables()); } @Override public int getValueSetCount() { return Iterables.size(getValueSets()); } @Override public int getVariableEntityCount() { return Iterables.size(getVariableEntities()); } // // Private methods // private String buildJoinTableName() { StringBuilder sb = new StringBuilder(); for (Iterator<ValueTable> it = getTables().iterator(); it.hasNext(); ) { sb.append(it.next().getName()); if (it.hasNext()) sb.append('-'); } return sb.toString(); } private synchronized Iterable<Variable> unionOfVariables() { if (unionOfVariables == null) { unionOfVariables = new LinkedHashSet<>(); if (!variableAnalysed) analyseVariables(); Collection<String> unionOfVariableNames = new LinkedHashSet<>(); for (ValueTable table : getTables()) { for (Variable variable : table.getVariables()) { // Add returns true if the set did not already contain the value if (unionOfVariableNames.add(variable.getName())) { unionOfVariables.add(variable); } } } } return unionOfVariables; } @NotNull private Multimap<Variable, ValueTable> getVariableTables() { if (!variableAnalysed) analyseVariables(); return variableTables; } @NotNull public synchronized List<ValueTable> getTablesWithVariable(@NotNull Variable joinableVariable) throws NoSuchVariableException { Collection<ValueTable> cachedTables = getVariableTables().get(joinableVariable); if (cachedTables == null) { throw new NoSuchVariableException(joinableVariable.getName()); } List<ValueTable> filteredList = cachedTables.stream().filter(t -> t != null).collect(Collectors.toList()); if (filteredList.isEmpty()) { throw new NoSuchVariableException(joinableVariable.getName()); } return filteredList; } /** * Get the list of tables that contribute to the entity list. * @return */ private List<ValueTable> getOuterTables() { return tables.stream().filter(table -> !innerTableReferences.contains(table.getTableReference())).collect(Collectors.toList()); } // // Private classes // private class ValueSetIterator implements Iterator<ValueSet> { private final Iterator<List<VariableEntity>> partitions; private Iterator<ValueSet> currentBatch; public ValueSetIterator(Iterable<VariableEntity> entities) { this.partitions = Iterables.partition(entities, ValueTable.ENTITY_BATCH_SIZE).iterator(); } @Override public boolean hasNext() { synchronized (partitions) { return partitions.hasNext() || (currentBatch != null && currentBatch.hasNext()); } } @Override public ValueSet next() { synchronized (partitions) { if (currentBatch == null || !currentBatch.hasNext()) { currentBatch = getValueSetsBatch(partitions.next()).getValueSets().iterator(); } return currentBatch.next(); } } private ValueSetBatch getValueSetsBatch(final List<VariableEntity> entities) { return new JoinValueSetBatch(JoinTable.this, entities); } } }