From b21ad7fee7804b19a131d9846b62518a6d465631 Mon Sep 17 00:00:00 2001 From: brinn <brinn> Date: Fri, 11 Sep 2009 18:34:48 +0000 Subject: [PATCH] add: MergedColumnDataReportingPlugin for column-based merging of TSV files change: rename MergedDataReportingPlugin -> MergedRowDataReportingPlugin SVN: 12557 --- .../AbstractDataMergingReportingPlugin.java | 197 +++++++++++++ .../DatasetModificationReportingPlugin.java | 4 +- .../plugins/demo/DemoReportingPlugin.java | 10 +- .../demo/MergedColumnDataReportingPlugin.java | 60 ++++ .../demo/MergedDataReportingPlugin.java | 264 ------------------ .../demo/MergedRowDataReportingPlugin.java | 83 ++++++ .../plugins/tasks/DatasetFileLines.java | 88 ++++++ .../tasks/IterativeTableModelBuilder.java | 144 ++++++++++ ...lder.java => SimpleTableModelBuilder.java} | 4 +- .../tasks/IterativeTableModelBuilderTest.java | 264 ++++++++++++++++++ 10 files changed, 845 insertions(+), 273 deletions(-) create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/AbstractDataMergingReportingPlugin.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedColumnDataReportingPlugin.java delete mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedDataReportingPlugin.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedRowDataReportingPlugin.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/DatasetFileLines.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilder.java rename datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/{TableModelBuilder.java => SimpleTableModelBuilder.java} (96%) create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilderTest.java diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/AbstractDataMergingReportingPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/AbstractDataMergingReportingPlugin.java new file mode 100644 index 00000000000..0f2f56b43af --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/AbstractDataMergingReportingPlugin.java @@ -0,0 +1,197 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.plugins.demo; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; + +import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.parser.ParserException; +import ch.systemsx.cisd.common.parser.ParsingException; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.DatasetFileLines; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.IReportingPluginTask; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.SimpleTableModelBuilder; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; + +/** + * Common super class of all tsv-based data merging reporting plugins. + * + * @author Bernd Rinn + */ +public abstract class AbstractDataMergingReportingPlugin extends AbstractDatastorePlugin implements + IReportingPluginTask +{ + private static final String FILE_INCLUDE_PATTERN = "file-include-pattern"; + + private static final String FILE_EXCLUDE_PATTERN = "file-exclude-pattern"; + + /** pattern for files that should be excluded (e.g. data set properties files) */ + public final static String DEFAULT_EXCLUDED_FILE_NAMES_PATTERN = ".*\\.tsv"; + + private final String excludePattern; + + private final String includePatternOrNull; + + protected AbstractDataMergingReportingPlugin(Properties properties, File storeRoot) + { + super(properties, storeRoot); + final String excludePatternOrNull = properties.getProperty(FILE_EXCLUDE_PATTERN); + if (excludePatternOrNull == null) + { + this.excludePattern = DEFAULT_EXCLUDED_FILE_NAMES_PATTERN; + } else + { + this.excludePattern = excludePatternOrNull; + } + this.includePatternOrNull = properties.getProperty(FILE_INCLUDE_PATTERN); + } + + protected String[] getHeaderTitles(DatasetDescription dataset) + { + File dir = getOriginalDir(dataset); + final DatasetFileLines lines = loadFromDirectory(dataset, dir); + return lines.getHeaderTokens(); + } + + protected static void addDataRows(SimpleTableModelBuilder builder, DatasetDescription dataset, + List<String[]> dataLines) + { + String datasetCode = dataset.getDatasetCode(); + for (String[] dataTokens : dataLines) + { + addDataRow(builder, datasetCode, dataTokens); + } + } + + protected static void addDataRow(SimpleTableModelBuilder builder, String datasetCode, + String[] dataTokens) + { + List<String> row = new ArrayList<String>(); + row.add(datasetCode); + row.addAll(Arrays.asList(dataTokens)); + builder.addRow(row); + } + + /** + * Loads {@link DatasetFileLines} from the file found in the specified directory. + * + * @throws IOExceptionUnchecked if a {@link IOException} has occurred. + */ + protected DatasetFileLines loadFromDirectory(DatasetDescription dataset, final File dir) + throws ParserException, ParsingException, IllegalArgumentException, + IOExceptionUnchecked + { + assert dir != null : "Given file must not be null"; + assert dir.isDirectory() : "Given file '" + dir.getAbsolutePath() + "' is not a directory."; + + File[] datasetFiles = FileUtilities.listFiles(dir); + List<File> datasetFilesToMerge = new ArrayList<File>(); + for (File datasetFile : datasetFiles) + { + if (datasetFile.isDirectory()) + { + // recursively go down the directories + return loadFromDirectory(dataset, datasetFile); + } else + { + // exclude files with properties + if (isFileExcluded(datasetFile) == false) + { + datasetFilesToMerge.add(datasetFile); + } + } + + } + if (datasetFilesToMerge.size() != 1) + { + throw UserFailureException + .fromTemplate( + "Directory with Data Set '%s' data ('%s') should contain exactly 1 file with data but %s files were found.", + dataset.getDatasetCode(), dir.getAbsolutePath(), datasetFilesToMerge + .size()); + } else + { + return loadFromFile(dataset, datasetFilesToMerge.get(0)); + } + } + + /** + * Loads {@link DatasetFileLines} from the specified tab file. + * + * @throws IOExceptionUnchecked if a {@link IOException} has occurred. + */ + protected DatasetFileLines loadFromFile(DatasetDescription dataset, final File file) + throws ParserException, ParsingException, IllegalArgumentException, + IOExceptionUnchecked + { + assert file != null : "Given file must not be null"; + assert file.isFile() : "Given file '" + file.getAbsolutePath() + "' is not a file."; + + FileReader reader = null; + try + { + reader = new FileReader(file); + return load(dataset, reader, file); + } catch (final IOException ex) + { + throw new IOExceptionUnchecked(ex); + } finally + { + IOUtils.closeQuietly(reader); + } + } + + /** + * Loads data from the specified reader. + * + * @throws IOException + */ + @SuppressWarnings("unchecked") + protected DatasetFileLines load(final DatasetDescription dataset, final Reader reader, + final File file) throws ParserException, ParsingException, IllegalArgumentException, + IOException + { + assert reader != null : "Unspecified reader"; + + final List<String> lines = IOUtils.readLines(reader); + return new DatasetFileLines(file, dataset, lines); + } + + protected boolean isFileExcluded(File file) + { + if (includePatternOrNull != null) + { + return file.getName().matches(includePatternOrNull) == false; + } else + { + return file.getName().matches(excludePattern); + } + } + + private static final long serialVersionUID = 1L; + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DatasetModificationReportingPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DatasetModificationReportingPlugin.java index 791a69545ec..d3e4b579d5a 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DatasetModificationReportingPlugin.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DatasetModificationReportingPlugin.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Properties; import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.IReportingPluginTask; -import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.TableModelBuilder; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.SimpleTableModelBuilder; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnType; import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; @@ -46,7 +46,7 @@ public class DatasetModificationReportingPlugin extends AbstractDatastorePlugin public TableModel createReport(List<DatasetDescription> datasets) { - TableModelBuilder builder = new TableModelBuilder(); + SimpleTableModelBuilder builder = new SimpleTableModelBuilder(); builder.addHeader("File", TableModelColumnType.TEXT); builder.addHeader("Modification date", TableModelColumnType.DATE); for (DatasetDescription dataset : datasets) diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DemoReportingPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DemoReportingPlugin.java index 2711dc5f4ac..135f9c9bc9e 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DemoReportingPlugin.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/DemoReportingPlugin.java @@ -25,7 +25,7 @@ import org.apache.commons.io.FileUtils; import ch.systemsx.cisd.common.filesystem.FileUtilities; import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.IReportingPluginTask; -import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.TableModelBuilder; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.SimpleTableModelBuilder; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnType; import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; @@ -46,7 +46,7 @@ public class DemoReportingPlugin extends AbstractDatastorePlugin implements IRep public TableModel createReport(List<DatasetDescription> datasets) { - TableModelBuilder builder = new TableModelBuilder(); + SimpleTableModelBuilder builder = new SimpleTableModelBuilder(); builder.addHeader("Dataset code", TableModelColumnType.TEXT); builder.addHeader("Name", TableModelColumnType.TEXT); builder.addHeader("Size", TableModelColumnType.INTEGER); @@ -64,7 +64,7 @@ public class DemoReportingPlugin extends AbstractDatastorePlugin implements IRep return builder.getTableModel(); } - private static void describe(TableModelBuilder builder, DatasetDescription dataset, File file) + private static void describe(SimpleTableModelBuilder builder, DatasetDescription dataset, File file) { if (file.isFile()) { @@ -79,14 +79,14 @@ public class DemoReportingPlugin extends AbstractDatastorePlugin implements IRep } } - private void describeUnknown(TableModelBuilder builder, DatasetDescription dataset, File file) + private void describeUnknown(SimpleTableModelBuilder builder, DatasetDescription dataset, File file) { String datasetCode = dataset.getDatasetCode(); List<String> row = Arrays.asList(datasetCode, file.getName(), "[does not exist]"); builder.addRow(row); } - private static void describeFile(TableModelBuilder builder, DatasetDescription dataset, + private static void describeFile(SimpleTableModelBuilder builder, DatasetDescription dataset, File file) { List<String> row = diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedColumnDataReportingPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedColumnDataReportingPlugin.java new file mode 100644 index 00000000000..18afb09dc58 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedColumnDataReportingPlugin.java @@ -0,0 +1,60 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.plugins.demo; + +import java.io.File; +import java.util.List; +import java.util.Properties; + +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.DatasetFileLines; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.IterativeTableModelBuilder; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; + +/** + * Reporting plugin that concatenates column tabular files of all data sets, using a + * <code>mergeColumn</code> to identify matching rows. + * + * @author Bernd Rinn + */ +public class MergedColumnDataReportingPlugin extends AbstractDataMergingReportingPlugin +{ + private static final long serialVersionUID = 1L; + + private static final String ROW_ID_COLUMN_HEADER = "row-id-column-header"; + + private final String rowIdentifierColumnHeader; + + public MergedColumnDataReportingPlugin(Properties properties, File storeRoot) + { + super(properties, storeRoot); + rowIdentifierColumnHeader = properties.getProperty(ROW_ID_COLUMN_HEADER); + } + + public TableModel createReport(List<DatasetDescription> datasets) + { + final IterativeTableModelBuilder builder = + new IterativeTableModelBuilder(rowIdentifierColumnHeader); + for (DatasetDescription dataset : datasets) + { + final DatasetFileLines lines = loadFromDirectory(dataset, getOriginalDir(dataset)); + builder.addFile(lines); + } + return builder.getTableModel(); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedDataReportingPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedDataReportingPlugin.java deleted file mode 100644 index 803c56c41a0..00000000000 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedDataReportingPlugin.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright 2009 ETH Zuerich, CISD - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.systemsx.cisd.openbis.dss.generic.server.plugins.demo; - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Properties; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; - -import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked; -import ch.systemsx.cisd.common.exceptions.UserFailureException; -import ch.systemsx.cisd.common.filesystem.FileUtilities; -import ch.systemsx.cisd.common.parser.ParserException; -import ch.systemsx.cisd.common.parser.ParsingException; -import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.IReportingPluginTask; -import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.TableModelBuilder; -import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; -import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnType; -import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; - -/** - * Reporting plugin that concatenates tabular files of all data sets (stripping the header lines of - * all but the first file) and delivers the result back in the table model. Each row has additional - * Data Set code column. - * - * @author Piotr Buczek - */ -public class MergedDataReportingPlugin extends AbstractDatastorePlugin implements - IReportingPluginTask -{ - - private static final long serialVersionUID = 1L; - /** pattern for files that should be excluded (e.g. data set properties files) */ - public final static String EXCLUDED_FILE_NAMES_PATTERN = ".*\\.tsv"; - - public MergedDataReportingPlugin(Properties properties, File storeRoot) - { - super(properties, storeRoot); - } - - public TableModel createReport(List<DatasetDescription> datasets) - { - TableModelBuilder builder = new TableModelBuilder(); - builder.addHeader("Data Set Code", TableModelColumnType.TEXT); - if (datasets.isEmpty() == false) - { - final DatasetDescription firstDataset = datasets.get(0); - final String[] titles = getHeaderTitles(firstDataset); - for (String title : titles) - { - builder.addHeader(title, TableModelColumnType.TEXT); - } - for (DatasetDescription dataset : datasets) - { - final File dir = getOriginalDir(dataset); - final DatasetFileLines lines = SimpleTabFileLoader.loadFromDirectory(dataset, dir); - if (Arrays.equals(titles, lines.getHeaderTokens()) == false) - { - throw UserFailureException.fromTemplate( - "All Data Set files should have the same headers, " - + "but file header of '%s': \n\t '%s' " - + "is different than file header of '%s': \n\t '%s'.", - firstDataset.getDatasetCode(), StringUtils.join(titles, "\t"), dataset - .getDatasetCode(), StringUtils.join(lines.getHeaderTokens(), - "\t")); - } - addDataRows(builder, dataset, lines.getDataLines()); - } - } - - return builder.getTableModel(); - } - - private String[] getHeaderTitles(DatasetDescription dataset) - { - File dir = getOriginalDir(dataset); - DatasetFileLines lines = SimpleTabFileLoader.loadFromDirectory(dataset, dir); - return lines.getHeaderTokens(); - } - - private static void addDataRows(TableModelBuilder builder, DatasetDescription dataset, - List<String[]> dataLines) - { - String datasetCode = dataset.getDatasetCode(); - for (String[] dataTokens : dataLines) - { - addDataRow(builder, datasetCode, dataTokens); - } - } - - private static void addDataRow(TableModelBuilder builder, String datasetCode, - String[] dataTokens) - { - List<String> row = new ArrayList<String>(); - row.add(datasetCode); - row.addAll(Arrays.asList(dataTokens)); - builder.addRow(row); - } - - private static class SimpleTabFileLoader - { - /** - * Loads {@link DatasetFileLines} from the file found in the specified directory. - * - * @throws IOExceptionUnchecked if a {@link IOException} has occurred. - */ - private static DatasetFileLines loadFromDirectory(DatasetDescription dataset, final File dir) - throws ParserException, ParsingException, IllegalArgumentException, - IOExceptionUnchecked - { - assert dir != null : "Given file must not be null"; - assert dir.isDirectory() : "Given file '" + dir.getAbsolutePath() - + "' is not a directory."; - - File[] datasetFiles = FileUtilities.listFiles(dir); - List<File> datasetFilesToMerge = new ArrayList<File>(); - for (File datasetFile : datasetFiles) - { - if (datasetFile.isDirectory()) - { - // recursively go down the directories - return loadFromDirectory(dataset, datasetFile); - } else - { - // exclude files with properties - if (isFileExcluded(datasetFile) == false) - { - datasetFilesToMerge.add(datasetFile); - } - } - - } - if (datasetFilesToMerge.size() != 1) - { - throw UserFailureException - .fromTemplate( - "Directory with Data Set '%s' data ('%s') should contain exactly 1 file with data but %s files were found.", - dataset.getDatasetCode(), dir.getAbsolutePath(), - datasetFilesToMerge.size()); - } else - { - return loadFromFile(dataset, datasetFilesToMerge.get(0)); - } - } - - private static boolean isFileExcluded(File file) - { - return file.getName().matches(EXCLUDED_FILE_NAMES_PATTERN); - } - - /** - * Loads {@link DatasetFileLines} from the specified tab file. - * - * @throws IOExceptionUnchecked if a {@link IOException} has occurred. - */ - private static DatasetFileLines loadFromFile(DatasetDescription dataset, final File file) - throws ParserException, ParsingException, IllegalArgumentException, - IOExceptionUnchecked - { - assert file != null : "Given file must not be null"; - assert file.isFile() : "Given file '" + file.getAbsolutePath() + "' is not a file."; - - FileReader reader = null; - try - { - reader = new FileReader(file); - return load(dataset, reader); - } catch (final IOException ex) - { - throw new IOExceptionUnchecked(ex); - } finally - { - IOUtils.closeQuietly(reader); - } - } - - /** - * Loads data from the specified reader. - * - * @throws IOException - */ - @SuppressWarnings("unchecked") - private static DatasetFileLines load(DatasetDescription dataset, final Reader reader) - throws ParserException, ParsingException, IllegalArgumentException, IOException - { - assert reader != null : "Unspecified reader"; - - final List<String> lines = IOUtils.readLines(reader); - return new DatasetFileLines(dataset, lines); - } - } - - private static class DatasetFileLines - { - private final String[] headerTokens; - - private final List<String[]> dataLines; - - public DatasetFileLines(DatasetDescription dataset, List<String> lines) - { - super(); - if (lines.size() < 2) - { - throw UserFailureException.fromTemplate( - "Data Set '%s' file should have at least 2 lines instead of %s.", dataset - .getDatasetCode(), lines.size()); - } - this.headerTokens = parseLine(lines.get(0)); - dataLines = new ArrayList<String[]>(lines.size()); - for (int i = 1; i < lines.size(); i++) - { - String[] dataTokens = parseLine(lines.get(i)); - if (headerTokens.length != dataTokens.length) - { - throw UserFailureException.fromTemplate( - "Number of columns in header (%s) does not match number of columns " - + "in %d data row (%s) in Data Set '%s' file.", - headerTokens.length, i, dataTokens.length, dataset.getDatasetCode()); - } - dataLines.add(dataTokens); - } - } - - /** splits line with '\t' and strips quotes from every token */ - private String[] parseLine(String line) - { - String[] tokens = line.split("\t"); - return StringUtils.stripAll(tokens, "'\""); - } - - public String[] getHeaderTokens() - { - return headerTokens; - } - - public List<String[]> getDataLines() - { - return dataLines; - } - - } - -} diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedRowDataReportingPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedRowDataReportingPlugin.java new file mode 100644 index 00000000000..5e3c9ca1494 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/demo/MergedRowDataReportingPlugin.java @@ -0,0 +1,83 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.plugins.demo; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.lang.StringUtils; + +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.DatasetFileLines; +import ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.SimpleTableModelBuilder; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnType; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; + +/** + * Reporting plugin that concatenates rows of tabular files of all data sets (stripping the header + * lines of all but the first file) and delivers the result back in the table model. Each row has + * additional Data Set code column. + * + * @author Piotr Buczek + */ +public class MergedRowDataReportingPlugin extends AbstractDataMergingReportingPlugin +{ + + private static final long serialVersionUID = 1L; + + public MergedRowDataReportingPlugin(Properties properties, File storeRoot) + { + super(properties, storeRoot); + } + + public TableModel createReport(List<DatasetDescription> datasets) + { + SimpleTableModelBuilder builder = new SimpleTableModelBuilder(); + builder.addHeader("Data Set Code", TableModelColumnType.TEXT); + if (datasets.isEmpty() == false) + { + final DatasetDescription firstDataset = datasets.get(0); + final String[] titles = getHeaderTitles(firstDataset); + for (String title : titles) + { + builder.addHeader(title, TableModelColumnType.TEXT); + } + for (DatasetDescription dataset : datasets) + { + final File dir = getOriginalDir(dataset); + final DatasetFileLines lines = loadFromDirectory(dataset, dir); + if (Arrays.equals(titles, lines.getHeaderTokens()) == false) + { + throw UserFailureException.fromTemplate( + "All Data Set files should have the same headers, " + + "but file header of '%s': \n\t '%s' " + + "is different than file header of '%s': \n\t '%s'.", + firstDataset.getDatasetCode(), StringUtils.join(titles, "\t"), dataset + .getDatasetCode(), StringUtils.join(lines.getHeaderTokens(), + "\t")); + } + addDataRows(builder, dataset, lines.getDataLines()); + } + } + + return builder.getTableModel(); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/DatasetFileLines.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/DatasetFileLines.java new file mode 100644 index 00000000000..35c045c178a --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/DatasetFileLines.java @@ -0,0 +1,88 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; + +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; + +/** + * Stores the lines of a tsv file. + * + * @author Bernd Rinn + */ +public class DatasetFileLines +{ + private final String[] headerTokens; + + private final List<String[]> dataLines; + + private final File file; + + public DatasetFileLines(File file, DatasetDescription dataset, List<String> lines) + { + this.file = file; + if (lines.size() < 2) + { + throw UserFailureException.fromTemplate( + "Data Set '%s' file should have at least 2 lines instead of %s.", dataset + .getDatasetCode(), lines.size()); + } + this.headerTokens = parseLine(lines.get(0)); + dataLines = new ArrayList<String[]>(lines.size()); + for (int i = 1; i < lines.size(); i++) + { + String[] dataTokens = parseLine(lines.get(i)); + if (headerTokens.length != dataTokens.length) + { + throw UserFailureException.fromTemplate( + "Number of columns in header (%s) does not match number of columns " + + "in %d data row (%s) in Data Set '%s' file.", + headerTokens.length, i, dataTokens.length, dataset.getDatasetCode()); + } + dataLines.add(dataTokens); + } + } + + /** splits line with '\t' and strips quotes from every token */ + private String[] parseLine(String line) + { + String[] tokens = line.split("\t"); + return StringUtils.stripAll(tokens, "'\""); + } + + public final File getFile() + { + return file; + } + + public String[] getHeaderTokens() + { + return headerTokens; + } + + public List<String[]> getDataLines() + { + return dataLines; + } + +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilder.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilder.java new file mode 100644 index 00000000000..044ce6d900e --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilder.java @@ -0,0 +1,144 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModelRow; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnHeader; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnType; + +/** + * A table model builder that can take new columns and rows iteratively. + * + * @author Bernd Rinn + */ +public class IterativeTableModelBuilder +{ + private final static Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, IterativeTableModelBuilderTest.class); + + private final String rowIdentifierColumnHeader; + + /** Set of row identifiers. */ + private final Set<String> rowIdentifiers = new LinkedHashSet<String>(); + + /** Map from column name to the map that maps row identifier to value. */ + private final Map<String, Map<String, String>> columnMap = + new LinkedHashMap<String, Map<String, String>>(); + + public IterativeTableModelBuilder(String rowIdentifierColumnHeader) + { + this.rowIdentifierColumnHeader = rowIdentifierColumnHeader; + } + + private int findIndexOfIdentifierColumn(DatasetFileLines lines) + { + int idx = 0; + for (String columnHeader : lines.getHeaderTokens()) + { + if (rowIdentifierColumnHeader.equals(columnHeader)) + { + return idx; + } + ++idx; + } + return -1; + } + + public void addFile(DatasetFileLines lines) + { + final int colIndexOfRowId = findIndexOfIdentifierColumn(lines); + if (colIndexOfRowId < 0) + { + operationLog.warn("Skip file '" + lines.getFile().getPath() + "' as it has no column '" + + rowIdentifierColumnHeader + "'."); + return; + } + final String[] columnHeaders = lines.getHeaderTokens(); + int colIdx = 0; + for (String columnHeader : columnHeaders) + { + // Does a column with that name already exist? + Map<String, String> column = columnMap.get(columnHeader); + if (column != null && colIdx == colIndexOfRowId) + { + addLineToColumn(lines, column, colIdx, colIndexOfRowId); + ++colIdx; + continue; + } + if (column != null) + { + columnHeader += "X"; + } + column = new HashMap<String, String>(); + columnMap.put(columnHeader, column); + addLineToColumn(lines, column, colIdx, colIndexOfRowId); + ++colIdx; + } + } + + private void addLineToColumn(DatasetFileLines lines, Map<String, String> column, int colIdx, + final int colIndexOfRowId) + { + for (String[] line : lines.getDataLines()) + { + final String rowId = line[colIndexOfRowId]; + rowIdentifiers.add(rowId); + column.put(rowId, line[colIdx]); + } + } + + /** + * Returns the table model that was built iteratively. + */ + public TableModel getTableModel() + { + final List<TableModelColumnHeader> headers = + new ArrayList<TableModelColumnHeader>(columnMap.size()); + final List<TableModelRow> rows = new ArrayList<TableModelRow>(rowIdentifiers.size()); + int idx = 0; + for (Entry<String, Map<String, String>> column : columnMap.entrySet()) + { + headers.add(new TableModelColumnHeader(column.getKey(), TableModelColumnType.TEXT, + idx++)); + } + for (String rowId : rowIdentifiers) + { + final List<String> rowValues = new ArrayList<String>(headers.size()); + for (TableModelColumnHeader header : headers) + { + final String valueOrNull = columnMap.get(header.getTitle()).get(rowId); + rowValues.add((valueOrNull == null) ? "" : valueOrNull); + } + rows.add(new TableModelRow(rowValues)); + } + return new TableModel(headers, rows); + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/TableModelBuilder.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/SimpleTableModelBuilder.java similarity index 96% rename from datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/TableModelBuilder.java rename to datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/SimpleTableModelBuilder.java index 237d9e901c4..31b6cd462c0 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/TableModelBuilder.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/SimpleTableModelBuilder.java @@ -29,13 +29,13 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelCo * * @author Tomasz Pylak */ -public class TableModelBuilder +public class SimpleTableModelBuilder { private List<TableModelRow> rows; private List<TableModelColumnHeader> header; - public TableModelBuilder() + public SimpleTableModelBuilder() { this.rows = new ArrayList<TableModelRow>(); this.header = new ArrayList<TableModelColumnHeader>(); diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilderTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilderTest.java new file mode 100644 index 00000000000..7d8ea1de2f6 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/plugins/tasks/IterativeTableModelBuilderTest.java @@ -0,0 +1,264 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks; + +import static org.testng.AssertJUnit.*; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.logging.LogInitializer; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModelRow; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModel.TableModelColumnHeader; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; + +/** + * Test cases for the {@link IterativeTableModelBuilder}. + * + * @author Bernd Rinn + */ +public class IterativeTableModelBuilderTest +{ + @BeforeTest + public void setUp() + { + LogInitializer.init(); + } + + @Test + public void testHappyCaseOneFile() + { + final IterativeTableModelBuilder builder = new IterativeTableModelBuilder("id"); + final DatasetDescription description = + new DatasetDescription("code", "location", "sampleCode", "groupCode"); + final DatasetFileLines lines = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines); + final TableModel model = builder.getTableModel(); + final List<TableModelColumnHeader> headers = model.getHeader(); + assertEquals(2, headers.size()); + assertEquals("id", headers.get(0).getTitle()); + assertEquals(0, headers.get(0).getIndex()); + assertEquals("val", headers.get(1).getTitle()); + assertEquals(1, headers.get(1).getIndex()); + final List<TableModelRow> rows = model.getRows(); + assertEquals(3, rows.size()); + final List<String> row0 = rows.get(0).getValues(); + assertEquals(2, row0.size()); + assertEquals("a", row0.get(0)); + assertEquals("A", row0.get(1)); + final List<String> row1 = rows.get(1).getValues(); + assertEquals(2, row1.size()); + assertEquals("b", row1.get(0)); + assertEquals("B", row1.get(1)); + final List<String> row2 = rows.get(2).getValues(); + assertEquals(2, row2.size()); + assertEquals("c", row2.get(0)); + assertEquals("C", row2.get(1)); + } + + @Test + public void testHappyCaseTwoFiles() + { + final IterativeTableModelBuilder builder = new IterativeTableModelBuilder("id"); + final DatasetDescription description = + new DatasetDescription("code", "location", "sampleCode", "groupCode"); + final DatasetFileLines lines = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines); + final DatasetFileLines lines2 = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval2", "a\tD", "b\tE", "c\tF")); + builder.addFile(lines2); + final TableModel model = builder.getTableModel(); + final List<TableModelColumnHeader> headers = model.getHeader(); + assertEquals(3, headers.size()); + assertEquals("id", headers.get(0).getTitle()); + assertEquals(0, headers.get(0).getIndex()); + assertEquals("val", headers.get(1).getTitle()); + assertEquals(1, headers.get(1).getIndex()); + assertEquals("val2", headers.get(2).getTitle()); + assertEquals(2, headers.get(2).getIndex()); + final List<TableModelRow> rows = model.getRows(); + assertEquals(3, rows.size()); + final List<String> row0 = rows.get(0).getValues(); + assertEquals(3, row0.size()); + assertEquals("a", row0.get(0)); + assertEquals("A", row0.get(1)); + assertEquals("D", row0.get(2)); + final List<String> row1 = rows.get(1).getValues(); + assertEquals(3, row1.size()); + assertEquals("b", row1.get(0)); + assertEquals("B", row1.get(1)); + assertEquals("E", row1.get(2)); + final List<String> row2 = rows.get(2).getValues(); + assertEquals(3, row2.size()); + assertEquals("c", row2.get(0)); + assertEquals("C", row2.get(1)); + assertEquals("F", row2.get(2)); + } + + @Test + public void testHappyCaseTwoFilesDifferentIds() + { + final IterativeTableModelBuilder builder = new IterativeTableModelBuilder("id"); + final DatasetDescription description = + new DatasetDescription("code", "location", "sampleCode", "groupCode"); + final DatasetFileLines lines = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines); + final DatasetFileLines lines2 = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval2", "d\tD", "b\tE", "f\tF")); + builder.addFile(lines2); + final TableModel model = builder.getTableModel(); + final List<TableModelColumnHeader> headers = model.getHeader(); + assertEquals(3, headers.size()); + assertEquals("id", headers.get(0).getTitle()); + assertEquals(0, headers.get(0).getIndex()); + assertEquals("val", headers.get(1).getTitle()); + assertEquals(1, headers.get(1).getIndex()); + assertEquals("val2", headers.get(2).getTitle()); + assertEquals(2, headers.get(2).getIndex()); + final List<TableModelRow> rows = model.getRows(); + assertEquals(5, rows.size()); + final List<String> row0 = rows.get(0).getValues(); + assertEquals(3, row0.size()); + assertEquals("a", row0.get(0)); + assertEquals("A", row0.get(1)); + assertEquals("", row0.get(2)); + final List<String> row1 = rows.get(1).getValues(); + assertEquals(3, row1.size()); + assertEquals("b", row1.get(0)); + assertEquals("B", row1.get(1)); + assertEquals("E", row1.get(2)); + final List<String> row2 = rows.get(2).getValues(); + assertEquals(3, row2.size()); + assertEquals("c", row2.get(0)); + assertEquals("C", row2.get(1)); + assertEquals("", row2.get(2)); + final List<String> row3 = rows.get(3).getValues(); + assertEquals(3, row3.size()); + assertEquals("d", row3.get(0)); + assertEquals("", row3.get(1)); + assertEquals("D", row3.get(2)); + final List<String> row4 = rows.get(4).getValues(); + assertEquals(3, row4.size()); + assertEquals("f", row4.get(0)); + assertEquals("", row4.get(1)); + assertEquals("F", row4.get(2)); + } + + @Test + public void testFileTwice() + { + final IterativeTableModelBuilder builder = new IterativeTableModelBuilder("id"); + final DatasetDescription description = + new DatasetDescription("code", "location", "sampleCode", "groupCode"); + final DatasetFileLines lines = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines); + builder.addFile(lines); + final TableModel model = builder.getTableModel(); + final List<TableModelColumnHeader> headers = model.getHeader(); + assertEquals(3, headers.size()); + assertEquals("id", headers.get(0).getTitle()); + assertEquals(0, headers.get(0).getIndex()); + assertEquals("val", headers.get(1).getTitle()); + assertEquals(1, headers.get(1).getIndex()); + assertEquals("valX", headers.get(2).getTitle()); + assertEquals(2, headers.get(2).getIndex()); + final List<TableModelRow> rows = model.getRows(); + assertEquals(3, rows.size()); + final List<String> row0 = rows.get(0).getValues(); + assertEquals(3, row0.size()); + assertEquals("a", row0.get(0)); + assertEquals("A", row0.get(1)); + assertEquals("A", row0.get(2)); + final List<String> row1 = rows.get(1).getValues(); + assertEquals(3, row1.size()); + assertEquals("b", row1.get(0)); + assertEquals("B", row1.get(1)); + assertEquals("B", row1.get(2)); + final List<String> row2 = rows.get(2).getValues(); + assertEquals(3, row2.size()); + assertEquals("c", row2.get(0)); + assertEquals("C", row2.get(1)); + assertEquals("C", row2.get(2)); + } + + @Test + public void testNoIdColumn() + { + final IterativeTableModelBuilder builder = new IterativeTableModelBuilder("id"); + final DatasetDescription description = + new DatasetDescription("code", "location", "sampleCode", "groupCode"); + final DatasetFileLines lines = + new DatasetFileLines(new File("bad file - no id"), description, Arrays.asList( + "ID\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines); + final TableModel model = builder.getTableModel(); + assertEquals(0, model.getHeader().size()); + assertEquals(0, model.getRows().size()); + } + + @Test + public void testTwoFilesNoIdColumnInOneFile() + { + final IterativeTableModelBuilder builder = new IterativeTableModelBuilder("id"); + final DatasetDescription description = + new DatasetDescription("code", "location", "sampleCode", "groupCode"); + final DatasetFileLines lines = + new DatasetFileLines(new File("bad file - no id (2)"), description, Arrays.asList( + "ID\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines); + final DatasetFileLines lines2 = + new DatasetFileLines(new File("doesn't matter"), description, Arrays.asList( + "id\tval", "a\tA", "b\tB", "c\tC")); + builder.addFile(lines2); + final TableModel model = builder.getTableModel(); + final List<TableModelColumnHeader> headers = model.getHeader(); + assertEquals(2, headers.size()); + assertEquals("id", headers.get(0).getTitle()); + assertEquals(0, headers.get(0).getIndex()); + assertEquals("val", headers.get(1).getTitle()); + assertEquals(1, headers.get(1).getIndex()); + final List<TableModelRow> rows = model.getRows(); + assertEquals(3, rows.size()); + final List<String> row0 = rows.get(0).getValues(); + assertEquals(2, row0.size()); + assertEquals("a", row0.get(0)); + assertEquals("A", row0.get(1)); + final List<String> row1 = rows.get(1).getValues(); + assertEquals(2, row1.size()); + assertEquals("b", row1.get(0)); + assertEquals("B", row1.get(1)); + final List<String> row2 = rows.get(2).getValues(); + assertEquals(2, row2.size()); + assertEquals("c", row2.get(0)); + assertEquals("C", row2.get(1)); + } +} -- GitLab