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