/* * 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.support; import java.io.IOException; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Collection; import java.util.LinkedList; import java.util.Set; import javax.annotation.Nullable; import org.obiba.magma.Datasource; import org.obiba.magma.DatasourceCopierProgressListener; import org.obiba.magma.MagmaEngine; import org.obiba.magma.Value; import org.obiba.magma.ValueSet; import org.obiba.magma.ValueTable; import org.obiba.magma.ValueTableWriter; import org.obiba.magma.ValueTableWriter.ValueSetWriter; import org.obiba.magma.ValueTableWriter.VariableWriter; import org.obiba.magma.Variable; import org.obiba.magma.VariableEntity; import org.obiba.magma.support.MultiplexingValueTableWriter.MultiplexedValueSetWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; @SuppressWarnings("UnusedDeclaration") public class DatasourceCopier { private static final Logger log = LoggerFactory.getLogger(DatasourceCopier.class); @SuppressWarnings("ParameterHidesMemberVariable") public static class Builder { private DatasourceCopier copier = new DatasourceCopier(); public Builder() { } public static Builder newCopier() { return new Builder(); } public static Builder newCopier(DatasourceCopier copier) { Builder b = new Builder(); b.copier = copier; return b; } public Builder dontCopyValues() { copier.copyValues = false; return this; } public Builder dontCopyMetadata() { copier.copyMetadata = false; return this; } public Builder dontCopyNullValues() { copier.copyNullValues = false; return this; } public Builder copyNullValues(boolean shouldCopy) { copier.copyNullValues = shouldCopy; return this; } public Builder withListener(DatasourceCopyEventListener listener) { if(listener == null) throw new IllegalArgumentException("listener cannot be null"); copier.listeners.add(listener); return this; } public Builder withProgressListener(@Nullable DatasourceCopierProgressListener progressListener) { if(progressListener != null) withListener(new DatasourceCopyProgressListener(progressListener)); return this; } public Builder withLoggingListener() { copier.listeners.add(new LoggingListener()); return this; } public Builder withThroughtputListener() { copier.listeners.add(new ThroughputListener()); return this; } public Builder withVariableTransformer(VariableTransformer transformer) { copier.variableTransformer = transformer; return this; } public Builder withMultiplexingStrategy(MultiplexingStrategy strategy) { copier.multiplexer = strategy; return this; } public DatasourceCopier build() { return new DatasourceCopier(copier); } private static class DatasourceCopyProgressListener implements DatasourceCopyValueSetEventListener { private final DatasourceCopierProgressListener progressListener; private long entitiesToCopy = 0; private long entitiesCopied = 0; private int nextPercentIncrement = 0; private DatasourceCopyProgressListener(DatasourceCopierProgressListener progressListener) { this.progressListener = progressListener; } @Override public void onValueSetCopy(ValueTable source, ValueSet valueSet) { if (entitiesToCopy == 0) { entitiesToCopy = source.getValueSetCount(); } } @Override public void onValueSetCopied(ValueTable source, ValueSet valueSet, String... tables) { entitiesCopied++; printProgress(source); } private void printProgress(ValueTable source) { try { if(entitiesToCopy > 0) { int percentComplete = (int) (entitiesCopied / (double) entitiesToCopy * 100); if(percentComplete >= nextPercentIncrement) { log.info("Copy {}% complete.", percentComplete); progressListener.status(source.getName(), entitiesCopied, entitiesToCopy, percentComplete); nextPercentIncrement = percentComplete + 1; } } } catch(RuntimeException e) { // Ignore } } } } private boolean copyNullValues = true; private boolean copyMetadata = true; private boolean copyValues = true; private Collection<DatasourceCopyEventListener> listeners = new LinkedList<>(); private VariableTransformer variableTransformer = new NoOpTransformer(); private MultiplexingStrategy multiplexer = null; private DatasourceCopier() { } private DatasourceCopier(DatasourceCopier other) { copyNullValues = other.copyNullValues; copyMetadata = other.copyMetadata; copyValues = other.copyValues; listeners = ImmutableList.copyOf(other.listeners); variableTransformer = other.variableTransformer; multiplexer = other.multiplexer; } public void copy(String sourceDatasource, String destinationDatasource) throws IOException { copy(MagmaEngine.get().getDatasource(sourceDatasource), MagmaEngine.get().getDatasource(destinationDatasource)); } public void copy(Datasource source, Datasource destination) throws IOException { if(source == destination) { // Don't copyMetadata on itself! The caller probably didn't want to really do this, did they? log.warn( "Invoked Datasource to Datasource copyMetadata with the same Datasource instance as sourceDatasource and destinationDatasource. " + "Nothing copied to or from Datasource '{}'.", source.getName()); return; } Set<ValueTable> tables = source.getValueTables(); int nbTables = tables.size(); log.info("Copying Datasource '{}' to '{}' ({} tables)", source.getName(), destination.getName(), nbTables); int i = 1; for(ValueTable table : tables) { log.debug("Copy table {} / {}", i++, nbTables); copy(table, destination); } } public void copy(ValueTable sourceTable, Datasource destination) throws IOException { copy(sourceTable, sourceTable.getName(), destination); } public void copy(ValueTable sourceTable, String destinationTableName, Datasource destination) throws IOException { log.info("Copying ValueTable '{}' to '{}.{}' (copyMetadata={}, copyValues={}).", sourceTable.getName(), destination.getName(), destinationTableName, copyMetadata, copyValues); Stopwatch stopwatch = null; if(log.isDebugEnabled()) { stopwatch = Stopwatch.createStarted(); log.debug(" --> {} variables, {} valueSets", sourceTable.getVariableCount(), sourceTable.getValueSetCount()); } try(ValueTableWriter tableWriter = innerValueTableWriter(sourceTable, destinationTableName, destination)) { String destTableName = destinationTableName; if (destination.hasValueTable(destinationTableName)) { // case there is a table renaming that applies (?) destTableName = destination.getValueTable(destinationTableName).getName(); } copy(sourceTable, destTableName, tableWriter); } if(log.isDebugEnabled()) { //noinspection ConstantConditions log.debug("Copied ValueTable '{}' n {}", sourceTable.getName(), stopwatch.stop()); } } private void copy(ValueTable sourceTable, String destinationTableName, ValueTableWriter tableWriter) throws IOException { copyMetadata(sourceTable, destinationTableName, tableWriter); copyValues(sourceTable, destinationTableName, tableWriter); } private void copyValues(ValueTable sourceTable, String destinationTableName, ValueTableWriter tableWriter) throws IOException { if(!copyValues) return; log.debug("Copy values from {} {}", sourceTable.getClass(), sourceTable.getName()); for(ValueSet valueSet : sourceTable.getValueSets()) { try(ValueSetWriter valueSetWriter = tableWriter.writeValueSet(valueSet.getVariableEntity())) { copyValues(sourceTable, valueSet, destinationTableName, valueSetWriter); } } } public void copyValues(ValueTable sourceTable, ValueSet valueSet, String destinationTableName, ValueSetWriter valueSetWriter) { if(!copyValues) return; notifyListeners(sourceTable, valueSet, false); for(Variable variable : sourceTable.getVariables()) { Value value = sourceTable.getValue(variable, valueSet); if(!value.isNull() || copyNullValues) { valueSetWriter.writeValue(variableTransformer.transform(variable), value); } } if(valueSetWriter instanceof MultiplexedValueSetWriter) { Set<String> tables = ((MultiplexedValueSetWriter) valueSetWriter).getTables(); notifyListeners(sourceTable, valueSet, true, tables.toArray(new String[tables.size()])); } else { notifyListeners(sourceTable, valueSet, true, destinationTableName); } } public void copyMetadata(ValueTable sourceTable, String destinationTableName, ValueTableWriter tableWriter) throws IOException { if(!copyMetadata) return; try(VariableWriter variableWriter = tableWriter.writeVariables()) { copyMetadata(sourceTable, variableWriter); } } public void copyMetadata(ValueTable sourceTable, VariableWriter variableWriter) { if(!copyMetadata) return; for(Variable variable : sourceTable.getVariables()) { notifyListeners(variable, false); variableWriter.writeVariable(variableTransformer.transform(variable)); notifyListeners(variable, true); } } void copyValues(ValueTable source, String tableName, ValueSet valueSet, Variable[] variables, Value[] values, ValueSetWriter vsw) { if(!copyValues) return; notifyListeners(source, valueSet, false); for(int i = 0; i < variables.length; i++) { Value value = values[i]; if(!value.isNull() || copyNullValues) { Variable variable = variables[i]; vsw.writeValue(variableTransformer.transform(variable), value); } } if(vsw instanceof MultiplexedValueSetWriter) { Set<String> tables = ((MultiplexedValueSetWriter) vsw).getTables(); notifyListeners(source, valueSet, true, tables.toArray(new String[tables.size()])); } else { notifyListeners(source, valueSet, true, tableName); } } public ValueTableWriter createValueTableWriter(ValueTable source, String destinationTableName, Datasource destination) { return destination.createWriter(destinationTableName, source.getEntityType()); } ValueTableWriter innerValueTableWriter(ValueTable source, String destinationTableName, Datasource destination) { return multiplexer == null ? createValueTableWriter(source, destinationTableName, destination) : new MultiplexingValueTableWriter(source, this, destination, multiplexer); } private void notifyListeners(Variable variable, boolean copied) { for(DatasourceCopyEventListener listener : listeners) { if(listener instanceof DatasourceCopyVariableEventListener) { DatasourceCopyVariableEventListener variableListener = (DatasourceCopyVariableEventListener) listener; if(copied) { variableListener.onVariableCopied(variable); } else { variableListener.onVariableCopy(variable); } } } } private void notifyListeners(ValueTable source, ValueSet valueSet, boolean copied, String... destination) { for(DatasourceCopyEventListener listener : listeners) { if(listener instanceof DatasourceCopyValueSetEventListener) { DatasourceCopyValueSetEventListener valueSetListener = (DatasourceCopyValueSetEventListener) listener; if(copied) { valueSetListener.onValueSetCopied(source, valueSet, destination); } else { valueSetListener.onValueSetCopy(source, valueSet); } } } } public void notifyListeners(ValueTable valueTable, String destinationTable, boolean copied) { for(DatasourceCopyEventListener listener : listeners) { if(listener instanceof DatasourceCopyValueTableEventListener) { DatasourceCopyValueTableEventListener valueTableListener = (DatasourceCopyValueTableEventListener) listener; if(copied) { valueTableListener.onValueTableCopied(valueTable, destinationTable); } else { valueTableListener.onValueTableCopy(valueTable, destinationTable); } } } } public interface DatasourceCopyEventListener {} public interface DatasourceCopyVariableEventListener extends DatasourceCopyEventListener { void onVariableCopy(Variable variable); void onVariableCopied(Variable variable); } public interface DatasourceCopyValueSetEventListener extends DatasourceCopyEventListener { void onValueSetCopy(ValueTable source, ValueSet valueSet); void onValueSetCopied(ValueTable source, ValueSet valueSet, String... tables); } public interface DatasourceCopyValueTableEventListener extends DatasourceCopyEventListener { void onValueTableCopy(ValueTable valueTable, String destinationTable); void onValueTableCopied(ValueTable valueTable, String destinationTable); } public interface VariableTransformer { Variable transform(Variable variable); } private static class NoOpTransformer implements VariableTransformer { @Override public Variable transform(Variable variable) { return variable; } } public interface MultiplexingStrategy { String multiplexVariable(Variable variable); String multiplexValueSet(VariableEntity entity, Variable variable); } private static class LoggingListener implements DatasourceCopyVariableEventListener, DatasourceCopyValueSetEventListener { @Override public void onValueSetCopied(ValueTable source, ValueSet valueSet, String... tables) { log.debug("Copied ValueSet for entity {}", valueSet.getVariableEntity()); } @Override public void onVariableCopied(Variable variable) { log.debug("Copied variable {}", variable.getName()); } @Override public void onValueSetCopy(ValueTable source, ValueSet valueSet) { log.debug("Copying ValueSet for entity {}", valueSet.getVariableEntity()); } @Override public void onVariableCopy(Variable variable) { log.debug("Copying variable {}", variable.getName()); } } private static class ThroughputListener implements DatasourceCopyValueSetEventListener { private static final NumberFormat TWO_DECIMAL_PLACES = DecimalFormat.getNumberInstance(); static { TWO_DECIMAL_PLACES.setMaximumFractionDigits(2); } private long count = 0; private long allDuration = 0; private long start; @Override public void onValueSetCopy(ValueTable source, ValueSet valueSet) { start = System.currentTimeMillis(); } @SuppressWarnings("MagicNumber") @Override public void onValueSetCopied(ValueTable source, ValueSet valueSet, String... tables) { long duration = System.currentTimeMillis() - start; allDuration += duration; count++; log.trace("ValueSet copied in {}s. Average copy duration for {} valueSets: {}s.", TWO_DECIMAL_PLACES.format(duration / 1000.0d), count, TWO_DECIMAL_PLACES.format(allDuration / (double) count / 1000.0d)); } } public boolean isCopyValues() { return copyValues; } public void setCopyValues(boolean copyValues) { this.copyValues = copyValues; } public boolean isCopyNullValues() { return copyNullValues; } public boolean isCopyMetadata() { return copyMetadata; } public void setCopyMetadata(boolean copyMetadata) { this.copyMetadata = copyMetadata; } }