From 73a0243dcf0f92331712b4c2ffa49154c62c2084 Mon Sep 17 00:00:00 2001 From: Jorrit Poelen Date: Thu, 1 Aug 2024 15:39:10 -0500 Subject: [PATCH] towards streaming reviews as outlined in https://github.com/Big-Bee-Network/bif/issues/1#issuecomment-2258640954 --- .../elton/cmd/CmdReview.java | 62 ++++++---- .../elton/cmd/CmdStream.java | 62 +++++++++- .../elton/cmd/ProgressCursorFactoryNoop.java | 16 +++ .../elton/cmd/ReviewReport.java | 14 +++ .../elton/cmd/WriterUtil.java | 12 ++ .../elton/cmd/CmdReviewTest.java | 2 +- .../elton/cmd/CmdStreamTest.java | 109 ++++++++++++++++++ 7 files changed, 248 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/globalbioticinteractions/elton/cmd/ProgressCursorFactoryNoop.java create mode 100644 src/test/java/org/globalbioticinteractions/elton/cmd/CmdStreamTest.java diff --git a/src/main/java/org/globalbioticinteractions/elton/cmd/CmdReview.java b/src/main/java/org/globalbioticinteractions/elton/cmd/CmdReview.java index e3e66db..9ac9989 100644 --- a/src/main/java/org/globalbioticinteractions/elton/cmd/CmdReview.java +++ b/src/main/java/org/globalbioticinteractions/elton/cmd/CmdReview.java @@ -34,6 +34,7 @@ import org.globalbioticinteractions.elton.Elton; import org.globalbioticinteractions.elton.util.DatasetRegistryUtil; import org.globalbioticinteractions.elton.util.NodeFactoryNull; +import org.globalbioticinteractions.elton.util.ProgressCursorFactory; import org.globalbioticinteractions.elton.util.ProgressUtil; import org.globalbioticinteractions.elton.util.SpecimenNull; import picocli.CommandLine; @@ -70,6 +71,7 @@ public class CmdReview extends CmdTabularWriterParams { public static final String LOG_FORMAT_STRING = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s"; public static final long LOG_NUMBER_OF_FIELDS = Arrays.stream(LOG_FORMAT_STRING.split("\t")).filter(x -> x.equals("%s")).count(); + public static final String REVIEWER_DEFAULT = "GloBI automated reviewer (elton-" + Elton.getVersionString() + ")"; @CommandLine.Option(names = {"-n", "--lines"}, description = "print first n number of lines") private Long maxLines = null; @@ -80,7 +82,7 @@ public class CmdReview extends CmdTabularWriterParams { private DateFactory dateFactory = Date::new; - private String reviewerName = "GloBI automated reviewer (elton-" + Elton.getVersionString() + ")"; + private String reviewerName = REVIEWER_DEFAULT; private String reviewId = UUID.randomUUID().toString(); @@ -112,7 +114,7 @@ public void run() { new ResourceServiceLocalAndRemote(factory) ); - review(DatasetRegistryUtil.NAMESPACE_LOCAL, registryLocal, factory); + review(DatasetRegistryUtil.NAMESPACE_LOCAL, registryLocal, factory, shouldSkipHeader()); } reviewCachedOrRemote(remoteNamespaces, factory); @@ -124,35 +126,35 @@ public void run() { private void reviewCachedOrRemote(List namespaces, InputStreamFactory inputStreamFactory) throws StudyImporterException { for (String namespace : namespaces) { - review(namespace, DatasetRegistryUtil.forCacheDir(getCacheDir(), new ResourceServiceLocal(inputStreamFactory)), inputStreamFactory); + review(namespace, DatasetRegistryUtil.forCacheDir(getCacheDir(), new ResourceServiceLocal(inputStreamFactory)), inputStreamFactory, shouldSkipHeader()); } } - private void review(String namespace, DatasetRegistry registry, InputStreamFactory inputStreamFactory) throws StudyImporterException { + private void review(String namespace, DatasetRegistry registry, InputStreamFactory inputStreamFactory, boolean shouldSkipHeader) throws StudyImporterException { ReviewReport report = createReport(namespace, CmdReview.this.reviewId, CmdReview.this.getReviewerName(), CmdReview.this.dateFactory); - ReviewReportLogger reviewReportLogger = new ReviewReportLogger(report, getStdout(), getMaxLines(), getProgressCursorFactory()); + ReviewReportLogger logger = new ReviewReportLogger(report, getStdout(), getMaxLines(), getProgressCursorFactory()); try { + getStderr().print("creating review [" + namespace + "]... "); + Dataset dataset = new DatasetFactory( registry, inputStreamFactory) .datasetFor(namespace); - String citationString = CitationUtil.citationFor(dataset); - if (StringUtils.startsWith(citationString, "<") - && StringUtils.endsWith(citationString, ">")) { - reviewReportLogger.warn(null, "no citation found for dataset at [" + dataset.getArchiveURI() + "]"); - } - NodeFactoryReview nodeFactory = new NodeFactoryReview(report.getInteractionCounter(), reviewReportLogger); - nodeFactory.getOrCreateDataset(dataset); - getStderr().print("creating review [" + namespace + "]... "); - if (!shouldSkipHeader()) { - logHeader(getStdout()); + if (!shouldSkipHeader) { + logReviewHeader(getStdout()); } + NodeFactoryReview nodeFactory = new NodeFactoryReview( + report.getInteractionCounter(), + logger, + getProgressCursorFactory() + ); + ParserFactoryLocal parserFactory = new ParserFactoryLocal(getClass()); DatasetImporterForRegistry studyImporter = new DatasetImporterForRegistry(parserFactory, nodeFactory, registry); - studyImporter.setLogger(reviewReportLogger); + studyImporter.setLogger(logger); DatasetImportUtil.importDataset( null, dataset, @@ -162,19 +164,19 @@ private void review(String namespace, DatasetRegistry registry, InputStreamFacto ); if (report.getInteractionCounter().get() == 0) { - reviewReportLogger.warn(null, "no interactions found"); + logger.warn(null, "no interactions found"); } getStderr().println("done."); log(null, dataset.getArchiveURI().toString(), ReviewCommentType.summary, report, getStdout()); } catch (DatasetRegistryException e) { - reviewReportLogger.warn(null, "no local repository at [" + getWorkDir().toString() + "]"); + logger.warn(null, "no local repository at [" + getWorkDir().toString() + "]"); getStderr().println("failed."); throw new StudyImporterException(e); } catch (Throwable e) { ByteArrayOutputStream out = new ByteArrayOutputStream(); e.printStackTrace(new PrintWriter(out)); e.printStackTrace(); - reviewReportLogger.severe(null, new String(out.toByteArray())); + logger.severe(null, new String(out.toByteArray())); throw new StudyImporterException(e); } finally { log(null, report.getInteractionCounter().get() + " interaction(s)", ReviewCommentType.summary, report, getStdout()); @@ -196,7 +198,7 @@ private ReviewReport createReport(String namespace, String reviewId1, String rev reviewId1, dateFactory1, reviewerName1, interactionCounter); } - private void logHeader(PrintStream out) { + public static void logReviewHeader(PrintStream out) { logReviewComment(out, "reviewId", "reviewDate", "reviewer", "namespace", "reviewCommentType", "reviewComment", "archiveURI", "referenceUrl", "institutionCode", "collectionCode", "collectionId", "catalogNumber", "occurrenceId", "sourceCitation", "dataContext"); } @@ -292,24 +294,40 @@ public void setDesiredReviewCommentTypes(List commentTypes) { desiredReviewCommentTypes = commentTypes; } - private class NodeFactoryReview extends NodeFactoryNull { + public static class NodeFactoryReview extends NodeFactoryNull { final AtomicLong counter; final ImportLogger importLogger; public NodeFactoryReview(AtomicLong counter, ImportLogger importLogger) { + this(counter, importLogger, new ProgressCursorFactoryNoop()); + } + + public NodeFactoryReview(AtomicLong counter, ImportLogger importLogger, ProgressCursorFactory progressCursorFactory) { this.counter = counter; this.importLogger = importLogger; + this.progressCursorFactory = progressCursorFactory; } + private ProgressCursorFactory progressCursorFactory; final Specimen specimen = new SpecimenNull() { @Override public void interactsWith(Specimen target, InteractType type, Location centroid) { long count = counter.get(); - ProgressUtil.logProgress(ProgressUtil.SPECIMEN_CREATED_PROGRESS_BATCH_SIZE, count, getProgressCursorFactory().createProgressCursor()); + ProgressUtil.logProgress(ProgressUtil.SPECIMEN_CREATED_PROGRESS_BATCH_SIZE, count, progressCursorFactory.createProgressCursor()); counter.getAndIncrement(); } }; + @Override + public Dataset getOrCreateDataset(Dataset dataset) { + String citationString = CitationUtil.citationFor(dataset); + if (StringUtils.startsWith(citationString, "<") + && StringUtils.endsWith(citationString, ">")) { + importLogger.warn(null, "no citation found for dataset at [" + dataset.getArchiveURI() + "]"); + } + return super.getOrCreateDataset(dataset); + } + @Override public Specimen createSpecimen(Study study, Taxon taxon) throws NodeFactoryException { diff --git a/src/main/java/org/globalbioticinteractions/elton/cmd/CmdStream.java b/src/main/java/org/globalbioticinteractions/elton/cmd/CmdStream.java index cd84d2f..0908825 100644 --- a/src/main/java/org/globalbioticinteractions/elton/cmd/CmdStream.java +++ b/src/main/java/org/globalbioticinteractions/elton/cmd/CmdStream.java @@ -9,8 +9,11 @@ import org.apache.commons.lang3.NotImplementedException; import org.eol.globi.data.ImportLogger; import org.eol.globi.data.NodeFactory; +import org.eol.globi.domain.LogContext; import org.eol.globi.tool.NullImportLogger; import org.globalbioticinteractions.elton.util.DatasetRegistryUtil; +import org.globalbioticinteractions.elton.util.ProgressCursor; +import org.globalbioticinteractions.elton.util.ProgressCursorFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -19,8 +22,10 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @CommandLine.Command( @@ -34,6 +39,10 @@ public class CmdStream extends CmdDefaultParams { private final static Logger LOG = LoggerFactory.getLogger(CmdStream.class); + public void setRecordType(String recordType) { + this.recordType = recordType; + } + @CommandLine.Option(names = {"--record-type"}, description = "record types (e.g., interaction, name, review)" ) @@ -51,6 +60,7 @@ public void run() { JsonNode jsonNode = new ObjectMapper().readTree(line); String namespace = jsonNode.at("/namespace").asText(DatasetRegistryUtil.NAMESPACE_LOCAL); if (StringUtils.isNotBlank(namespace)) { + ImportLoggerFactory loggerFactory = new ImportLoggerFactoryImpl(recordType, namespace, Arrays.asList(ReviewCommentType.values()), getStdout()); try { boolean shouldWriteHeader = isFirst.get(); StreamingDatasetsHandler namespaceHandler = new StreamingDatasetsHandler( @@ -58,13 +68,20 @@ public void run() { this.getCacheDir(), this.getStderr(), this.createInputStreamFactory(), - new NodeFactoryFactoryImpl(shouldWriteHeader, recordType), - new ImportLoggerFactoryImpl(recordType) + new NodeFactoryFactoryImpl(shouldWriteHeader, recordType, loggerFactory.createImportLogger()), + loggerFactory ); namespaceHandler.onNamespace(namespace); isFirst.set(false); } catch (Exception e) { - LOG.error("failed to add dataset associated with namespace [" + namespace + "]", e); + String msg = "failed to add dataset associated with namespace [" + namespace + "]"; + loggerFactory.createImportLogger().warn(new LogContext() { + @Override + public String toString() { + return "{ \"namespace\": \"" + namespace + "\" }"; + } + }, msg); + LOG.error(msg, e); } finally { FileUtils.forceDelete(new File(this.getCacheDir())); } @@ -81,9 +98,18 @@ public void run() { public static class ImportLoggerFactoryImpl implements ImportLoggerFactory { private final String recordType; - - public ImportLoggerFactoryImpl(String recordType) { + private final String namespace; + private final List desiredReviewCommentTypes; + private final PrintStream stdout; + + public ImportLoggerFactoryImpl(String recordType, + String namespace, + List desiredReviewCommentTypes, + PrintStream stdout) { this.recordType = recordType; + this.namespace = namespace; + this.desiredReviewCommentTypes = desiredReviewCommentTypes; + this.stdout = stdout; } @Override @@ -91,6 +117,22 @@ public ImportLogger createImportLogger() { ImportLogger logger; if (Arrays.asList("name", "interaction").contains(recordType)) { logger = new NullImportLogger(); + } else if (StringUtils.equals("review", recordType)) { + logger = new ReviewReportLogger( + new ReviewReport(namespace, desiredReviewCommentTypes), + stdout, + null, + new ProgressCursorFactory() { + @Override + public ProgressCursor createProgressCursor() { + return new ProgressCursor() { + @Override + public void increment() { + + } + }; + } + }); } else { throw new NotImplementedException("no import logger for [" + recordType + "] available yet."); } @@ -103,10 +145,12 @@ public class NodeFactoryFactoryImpl implements NodeFactorFactory { private final boolean shouldWriteHeader; private final String recordType; + private ImportLogger logger; - public NodeFactoryFactoryImpl(boolean shouldWriteHeader, String recordType) { + public NodeFactoryFactoryImpl(boolean shouldWriteHeader, String recordType, ImportLogger logger) { this.shouldWriteHeader = shouldWriteHeader; this.recordType = recordType; + this.logger = logger; } @Override @@ -116,10 +160,16 @@ public NodeFactory createNodeFactory() { factory = WriterUtil.nodeFactoryForInteractionWriting(shouldWriteHeader, getStdout()); } else if (StringUtils.equals("name", recordType)) { factory = WriterUtil.nodeFactoryForTaxonWriting(shouldWriteHeader, getStdout()); + } else if (StringUtils.equals("review", recordType)) { + factory = WriterUtil.nodeFactoryForReviewWriting(shouldWriteHeader, getStdout(), logger); } else { throw new NotImplementedException("no node factory for [" + recordType + "] available yet."); } return factory; } } + + public static class WriterFactory { + + } } diff --git a/src/main/java/org/globalbioticinteractions/elton/cmd/ProgressCursorFactoryNoop.java b/src/main/java/org/globalbioticinteractions/elton/cmd/ProgressCursorFactoryNoop.java new file mode 100644 index 0000000..924722e --- /dev/null +++ b/src/main/java/org/globalbioticinteractions/elton/cmd/ProgressCursorFactoryNoop.java @@ -0,0 +1,16 @@ +package org.globalbioticinteractions.elton.cmd; + +import org.globalbioticinteractions.elton.util.ProgressCursor; +import org.globalbioticinteractions.elton.util.ProgressCursorFactory; + +public class ProgressCursorFactoryNoop implements ProgressCursorFactory { + @Override + public ProgressCursor createProgressCursor() { + return new ProgressCursor() { + @Override + public void increment() { + // + } + }; + } +} diff --git a/src/main/java/org/globalbioticinteractions/elton/cmd/ReviewReport.java b/src/main/java/org/globalbioticinteractions/elton/cmd/ReviewReport.java index 1951bfb..bd78ffb 100644 --- a/src/main/java/org/globalbioticinteractions/elton/cmd/ReviewReport.java +++ b/src/main/java/org/globalbioticinteractions/elton/cmd/ReviewReport.java @@ -1,6 +1,8 @@ package org.globalbioticinteractions.elton.cmd; +import java.util.Date; import java.util.List; +import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; public class ReviewReport { @@ -15,6 +17,18 @@ public class ReviewReport { private final DateFactory dateFactory; private final String reviewerName; + ReviewReport(String namespace, List desiredReviewCommentTypes) { + this.infoCounter = new AtomicLong(0); + this.noteCounter = new AtomicLong(0); + this.interactionCounter = new AtomicLong(0); + this.namespace = namespace; + this.desiredReviewCommentTypes = desiredReviewCommentTypes; + this.lineCount = new AtomicLong(0); + this.dateFactory = () -> new Date(); + this.reviewId = UUID.randomUUID().toString(); + this.reviewerName = CmdReview.REVIEWER_DEFAULT; + } + ReviewReport(AtomicLong infoCounter, AtomicLong noteCounter, String namespace, List desiredReviewCommentTypes, AtomicLong lineCount, String reviewId, DateFactory dateFactory, String reviewerName, AtomicLong interactionCounter) { this.infoCounter = infoCounter; this.noteCounter = noteCounter; diff --git a/src/main/java/org/globalbioticinteractions/elton/cmd/WriterUtil.java b/src/main/java/org/globalbioticinteractions/elton/cmd/WriterUtil.java index 7f6b014..2897ebf 100644 --- a/src/main/java/org/globalbioticinteractions/elton/cmd/WriterUtil.java +++ b/src/main/java/org/globalbioticinteractions/elton/cmd/WriterUtil.java @@ -1,5 +1,6 @@ package org.globalbioticinteractions.elton.cmd; +import org.eol.globi.data.ImportLogger; import org.eol.globi.data.NodeFactory; import org.eol.globi.data.NodeFactoryException; import org.eol.globi.domain.Interaction; @@ -13,6 +14,7 @@ import org.globalbioticinteractions.elton.util.TaxonWriter; import java.io.PrintStream; +import java.util.concurrent.atomic.AtomicLong; public class WriterUtil { static NodeFactory nodeFactoryForInteractionWriting(boolean shouldWriteHeader, PrintStream stdout) { @@ -55,4 +57,14 @@ public Specimen createSpecimen(Study study, Taxon taxon) throws NodeFactoryExcep } }; } + + public static NodeFactory nodeFactoryForReviewWriting(boolean shouldWriteHeader, + PrintStream out, + ImportLogger importLogger) { + if (shouldWriteHeader) { + CmdReview.logReviewHeader(out); + } + return new CmdReview.NodeFactoryReview(new AtomicLong(0), importLogger); + } + } diff --git a/src/test/java/org/globalbioticinteractions/elton/cmd/CmdReviewTest.java b/src/test/java/org/globalbioticinteractions/elton/cmd/CmdReviewTest.java index 607fa3a..629d76d 100644 --- a/src/test/java/org/globalbioticinteractions/elton/cmd/CmdReviewTest.java +++ b/src/test/java/org/globalbioticinteractions/elton/cmd/CmdReviewTest.java @@ -244,7 +244,7 @@ public void runCheckLocalNoRepo() { try { cmdReview.run(); } finally { - assertThat(errOs.toString(), is("failed.\n")); + assertThat(errOs.toString(), is("creating review [local]... failed.\n")); } } diff --git a/src/test/java/org/globalbioticinteractions/elton/cmd/CmdStreamTest.java b/src/test/java/org/globalbioticinteractions/elton/cmd/CmdStreamTest.java new file mode 100644 index 0000000..13c8e7c --- /dev/null +++ b/src/test/java/org/globalbioticinteractions/elton/cmd/CmdStreamTest.java @@ -0,0 +1,109 @@ +package org.globalbioticinteractions.elton.cmd; + +import org.apache.commons.io.IOUtils; +import org.hamcrest.core.Is; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; + +public class CmdStreamTest { + + @Test + public void streamNothing() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream errorStream = new ByteArrayOutputStream(); + + CmdStream cmdStream = new CmdStream(); + cmdStream.setStdout(new PrintStream(outputStream)); + cmdStream.setStderr(new PrintStream(errorStream)); + cmdStream.setStdin(IOUtils.toInputStream("{}", StandardCharsets.UTF_8)); + cmdStream.run(); + + assertThat(new String(outputStream.toByteArray(), StandardCharsets.UTF_8), Is.is("")); + assertThat(new String(errorStream.toByteArray(), StandardCharsets.UTF_8), startsWith("tracking [local]...")); + assertThat(new String(errorStream.toByteArray(), StandardCharsets.UTF_8), endsWith("tracking [local]...")); + } + + @Test + public void streamSomeInteractions() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream errorStream = new ByteArrayOutputStream(); + + CmdStream cmdStream = new CmdStream(); + cmdStream.setStdout(new PrintStream(outputStream)); + cmdStream.setStderr(new PrintStream(errorStream)); + cmdStream.setStdin(IOUtils.toInputStream("{ \"url\": \"bla.tsv\", \"citation\": \"some citation\" }", StandardCharsets.UTF_8)); + cmdStream.run(); + + assertThat(new String(outputStream.toByteArray(), StandardCharsets.UTF_8), startsWith(headerInteractions())); + assertThat(new String(errorStream.toByteArray(), StandardCharsets.UTF_8), startsWith("tracking [local]...")); + } + + @Test + public void streamSomeInteractionsCustomNamespace() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream errorStream = new ByteArrayOutputStream(); + + CmdStream cmdStream = new CmdStream(); + cmdStream.setStdout(new PrintStream(outputStream)); + cmdStream.setStderr(new PrintStream(errorStream)); + cmdStream.setStdin(IOUtils.toInputStream("{ \"namespace\": \"name/space\", \"url\": \"bla.tsv\", \"citation\": \"some citation\" }", StandardCharsets.UTF_8)); + cmdStream.run(); + + assertThat(new String(outputStream.toByteArray(), StandardCharsets.UTF_8), startsWith(headerInteractions())); + assertThat(new String(errorStream.toByteArray(), StandardCharsets.UTF_8), startsWith("tracking [name/space]...")); + } + + @Test + public void streamSomeNames() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream errorStream = new ByteArrayOutputStream(); + + CmdStream cmdStream = new CmdStream(); + cmdStream.setRecordType("name"); + cmdStream.setStdout(new PrintStream(outputStream)); + cmdStream.setStderr(new PrintStream(errorStream)); + cmdStream.setStdin(IOUtils.toInputStream("{ \"format\": \"dwca\", \"url\": \"bla.tsv\", \"citation\": \"some citation\" }", StandardCharsets.UTF_8)); + cmdStream.run(); + + assertThat(new String(outputStream.toByteArray(), StandardCharsets.UTF_8), startsWith(headerNames())); + assertThat(new String(errorStream.toByteArray(), StandardCharsets.UTF_8), startsWith("tracking [local]...")); + } + + @Test + public void streamSomeReviewNotesNoData() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream errorStream = new ByteArrayOutputStream(); + + CmdStream cmdStream = new CmdStream(); + cmdStream.setRecordType("review"); + cmdStream.setStdout(new PrintStream(outputStream)); + cmdStream.setStderr(new PrintStream(errorStream)); + cmdStream.setStdin(IOUtils.toInputStream("{ \"format\": \"dwca\", \"url\": \"bla.tsv\", \"citation\": \"some citation\" }", StandardCharsets.UTF_8)); + cmdStream.run(); + + assertThat(new String(outputStream.toByteArray(), StandardCharsets.UTF_8), startsWith(headerReviewNotes())); + assertThat(new String(outputStream.toByteArray(), StandardCharsets.UTF_8), containsString("failed to add dataset associated with namespace [local]")); + assertThat(new String(errorStream.toByteArray(), StandardCharsets.UTF_8), startsWith("tracking [local]...")); + } + + private String headerNames() { + return "taxonId\ttaxonName\ttaxonRank\ttaxonPathIds\ttaxonPath\ttaxonPathNames\tnamespace\tcitation\tarchiveURI\tlastSeenAt\tcontentHash\teltonVersion"; + } + + private String headerInteractions() { + return "argumentTypeId\tsourceOccurrenceId\tsourceCatalogNumber\tsourceCollectionCode\tsourceCollectionId\tsourceInstitutionCode\tsourceTaxonId\tsourceTaxonName\tsourceTaxonRank\tsourceTaxonPathIds\tsourceTaxonPath\tsourceTaxonPathNames\tsourceBodyPartId\tsourceBodyPartName\tsourceLifeStageId\tsourceLifeStageName\tsourceSexId\tsourceSexName\tinteractionTypeId\tinteractionTypeName\ttargetOccurrenceId\ttargetCatalogNumber\ttargetCollectionCode\ttargetCollectionId\ttargetInstitutionCode\ttargetTaxonId\ttargetTaxonName\ttargetTaxonRank\ttargetTaxonPathIds\ttargetTaxonPath\ttargetTaxonPathNames\ttargetBodyPartId\ttargetBodyPartName\ttargetLifeStageId\ttargetLifeStageName\ttargetSexId\ttargetSexName\tbasisOfRecordId\tbasisOfRecordName\thttp://rs.tdwg.org/dwc/terms/eventDate\tdecimalLatitude\tdecimalLongitude\tlocalityId\tlocalityName\treferenceDoi\treferenceUrl\treferenceCitation\tnamespace\tcitation\tarchiveURI\tlastSeenAt\tcontentHash\teltonVersion"; + } + + private String headerReviewNotes() { + return "reviewId\treviewDate\treviewer\tnamespace\treviewCommentType\treviewComment\tarchiveURI\treferenceUrl\tinstitutionCode\tcollectionCode\tcollectionId\tcatalogNumber\toccurrenceId\tsourceCitation\tdataContext"; + } + +} \ No newline at end of file