From d58953e408f7b4d1b11daa119ee1068f7188fa49 Mon Sep 17 00:00:00 2001
From: Michael Davis <michael.davis@cern.ch>
Date: Mon, 17 Jun 2019 17:26:16 +0200
Subject: [PATCH] [cta-admin] Adds generic formatting for stream text output

---
 cmdline/CtaAdminCmd.cpp           |  51 +++++----
 cmdline/CtaAdminTextFormatter.cpp | 170 ++++++++++++++++++------------
 cmdline/CtaAdminTextFormatter.hpp | 111 +++++++++++++------
 3 files changed, 210 insertions(+), 122 deletions(-)

diff --git a/cmdline/CtaAdminCmd.cpp b/cmdline/CtaAdminCmd.cpp
index 6279e19ed8..cbd6ae7892 100644
--- a/cmdline/CtaAdminCmd.cpp
+++ b/cmdline/CtaAdminCmd.cpp
@@ -27,9 +27,14 @@
 #include <cmdline/CtaAdminTextFormatter.hpp>
 
 
-// global synchronisation flag between main thread and stream handler thread
+// GLOBAL VARIABLES : used to pass information between main thread and stream handler thread
+
+// global synchronisation flag
 std::atomic<bool> isHeaderSent(false);
 
+// initialise an output buffer of 5 lines
+cta::admin::TextFormatter formattedText(5);
+
 
 namespace XrdSsiPb {
 
@@ -83,17 +88,17 @@ void IStreamBuffer<cta::xrd::Data>::DataCallback(cta::xrd::Data record) const
    }
    // Format results in a tabular format for a human
    else switch(record.data_case()) {
-         case Data::kAflsItem:      CtaAdminTextFormatter::print(record.afls_item());    break;
-         case Data::kAflsSummary:   CtaAdminTextFormatter::print(record.afls_summary()); break;
-         case Data::kFrlsItem:      CtaAdminTextFormatter::print(record.frls_item());    break;
-         case Data::kFrlsSummary:   CtaAdminTextFormatter::print(record.frls_summary()); break;
-         case Data::kLpaItem:       CtaAdminTextFormatter::print(record.lpa_item());     break;
-         case Data::kLpaSummary:    CtaAdminTextFormatter::print(record.lpa_summary());  break;
-         case Data::kLprItem:       CtaAdminTextFormatter::print(record.lpr_item());     break;
-         case Data::kLprSummary:    CtaAdminTextFormatter::print(record.lpr_summary());  break;
-         case Data::kTplsItem:      CtaAdminTextFormatter::print(record.tpls_item());    break;
-         case Data::kTalsItem:      CtaAdminTextFormatter::print(record.tals_item());    break;
-         case Data::kRelsItem:      CtaAdminTextFormatter::print(record.rels_item());    break;
+         case Data::kAflsItem:      formattedText.print(record.afls_item());     break;
+         case Data::kAflsSummary:   TextFormatter::print(record.afls_summary()); break;
+         case Data::kFrlsItem:      TextFormatter::print(record.frls_item());    break;
+         case Data::kFrlsSummary:   TextFormatter::print(record.frls_summary()); break;
+         case Data::kLpaItem:       TextFormatter::print(record.lpa_item());     break;
+         case Data::kLpaSummary:    TextFormatter::print(record.lpa_summary());  break;
+         case Data::kLprItem:       TextFormatter::print(record.lpr_item());     break;
+         case Data::kLprSummary:    TextFormatter::print(record.lpr_summary());  break;
+         case Data::kTplsItem:      TextFormatter::print(record.tpls_item());    break;
+         case Data::kTalsItem:      TextFormatter::print(record.tals_item());    break;
+         case Data::kRelsItem:      TextFormatter::print(record.rels_item());    break;
          default:
             throw std::runtime_error("Received invalid stream data from CTA Frontend.");
    }
@@ -227,17 +232,17 @@ void CtaAdminCmd::send() const
          std::cout << response.message_txt();
          // Print streaming response header
          if(!isJson()) switch(response.show_header()) {
-            case HeaderType::ARCHIVEFILE_LS:               CtaAdminTextFormatter::printAfLsHeader(); break;
-            case HeaderType::ARCHIVEFILE_LS_SUMMARY:       CtaAdminTextFormatter::printAfLsSummaryHeader(); break;
-            case HeaderType::FAILEDREQUEST_LS:             CtaAdminTextFormatter::printFrLsHeader(); break;
-            case HeaderType::FAILEDREQUEST_LS_SUMMARY:     CtaAdminTextFormatter::printFrLsSummaryHeader(); break;
-            case HeaderType::LISTPENDINGARCHIVES:          CtaAdminTextFormatter::printLpaHeader(); break;
-            case HeaderType::LISTPENDINGARCHIVES_SUMMARY:  CtaAdminTextFormatter::printLpaSummaryHeader(); break;
-            case HeaderType::LISTPENDINGRETRIEVES:         CtaAdminTextFormatter::printLprHeader(); break;
-            case HeaderType::LISTPENDINGRETRIEVES_SUMMARY: CtaAdminTextFormatter::printLprSummaryHeader(); break;
-            case HeaderType::TAPEPOOL_LS:                  CtaAdminTextFormatter::printTpLsHeader(); break;
-            case HeaderType::TAPE_LS:                      CtaAdminTextFormatter::printTapeLsHeader(); break;
-            case HeaderType::REPACK_LS:                    CtaAdminTextFormatter::printRepackLsHeader(); break;
+            case HeaderType::ARCHIVEFILE_LS:               formattedText.printAfLsHeader(); break;
+            case HeaderType::ARCHIVEFILE_LS_SUMMARY:       TextFormatter::printAfLsSummaryHeader(); break;
+            case HeaderType::FAILEDREQUEST_LS:             TextFormatter::printFrLsHeader(); break;
+            case HeaderType::FAILEDREQUEST_LS_SUMMARY:     TextFormatter::printFrLsSummaryHeader(); break;
+            case HeaderType::LISTPENDINGARCHIVES:          TextFormatter::printLpaHeader(); break;
+            case HeaderType::LISTPENDINGARCHIVES_SUMMARY:  TextFormatter::printLpaSummaryHeader(); break;
+            case HeaderType::LISTPENDINGRETRIEVES:         TextFormatter::printLprHeader(); break;
+            case HeaderType::LISTPENDINGRETRIEVES_SUMMARY: TextFormatter::printLprSummaryHeader(); break;
+            case HeaderType::TAPEPOOL_LS:                  TextFormatter::printTpLsHeader(); break;
+            case HeaderType::TAPE_LS:                      TextFormatter::printTapeLsHeader(); break;
+            case HeaderType::REPACK_LS:                    TextFormatter::printRepackLsHeader(); break;
             case HeaderType::NONE:
             default:                                       break;
          }
diff --git a/cmdline/CtaAdminTextFormatter.cpp b/cmdline/CtaAdminTextFormatter.cpp
index 2c344e9a93..10c11a5808 100644
--- a/cmdline/CtaAdminTextFormatter.cpp
+++ b/cmdline/CtaAdminTextFormatter.cpp
@@ -24,58 +24,98 @@
 
 namespace cta { namespace admin {
 
-void CtaAdminTextFormatter::printAfLsHeader()
+const std::string TextFormatter::TEXT_RED    = "\x1b[31;1m";
+const std::string TextFormatter::TEXT_NORMAL = "\x1b[0m";
+
+/*
+ * Convert time to string
+ *
+ * NOTE: ctime is not thread-safe!
+ */
+std::string timeToString(const time_t &time)
 {
-   std::cout << TEXT_RED
-             << std::setfill(' ') << std::setw(11) << std::right << "archive id"     << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << "copy no"        << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << "vid"            << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << "fseq"           << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << "block id"       << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << "instance"       << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << "disk id"        << ' '
-             << std::setfill(' ') << std::setw(12) << std::right << "size"           << ' '
-             << std::setfill(' ') << std::setw(13) << std::right << "checksum type"  << ' '
-             << std::setfill(' ') << std::setw(14) << std::right << "checksum value" << ' '
-             << std::setfill(' ') << std::setw(16) << std::right << "storage class"  << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << "owner"          << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << "group"          << ' '
-             << std::setfill(' ') << std::setw(13) << std::right << "creation time"  << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << "ss vid"         << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << "ss fseq"        << ' '
-                                                                 << "path"
-             << TEXT_NORMAL << std::endl;
+  std::string timeString(ctime(&time));
+  timeString.resize(timeString.size()-1); //remove newline
+  return timeString;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::ArchiveFileLsItem &afls_item)
+
+void TextFormatter::flush() {
+  if(m_outputBuffer.empty()) return;
+
+  auto numCols = m_outputBuffer.front().size();
+  std::vector<unsigned int> colSize(numCols);
+
+  // Calculate column widths
+  for(auto &l : m_outputBuffer) {
+    if(l.size() != numCols) throw std::runtime_error("TextFormatter::flush(): incorrect number of columns");
+    for(size_t c = 0; c < l.size(); ++c) {
+      if(colSize.at(c) < l.at(c).size()) colSize[c] = l.at(c).size();
+    }
+  }
+
+  // Output columns
+  for(auto &l : m_outputBuffer) {
+    for(size_t c = 0; c < l.size(); ++c) {
+      std::cout << std::setfill(' ')
+                << std::setw(colSize.at(c)+1)
+                << std::right
+                << l.at(c) << ' ';
+    }
+    std::cout << std::endl;
+  }
+
+  // Empty buffer
+  m_outputBuffer.clear();
+}
+
+
+void TextFormatter::printAfLsHeader() {
+  push_back(
+    "archive id",
+    "copy no",
+    "vid",
+    "fseq",
+    "block id",
+    "instance",
+    "disk id",
+    "size",
+    "checksum type",
+    "checksum value",
+    "storage class",
+    "owner",
+    "group",
+    "creation time",
+    "ss vid",
+    "ss fseq",
+    "path"
+  );
+}
+
+void TextFormatter::print(const cta::admin::ArchiveFileLsItem &afls_item)
 {
-   std::cout << std::setfill(' ') << std::setw(11) << std::right << afls_item.af().archive_id()    << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << afls_item.copy_nb()            << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << afls_item.tf().vid()           << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << afls_item.tf().f_seq()         << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << afls_item.tf().block_id()      << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << afls_item.af().disk_instance() << ' '
-             << std::setfill(' ') << std::setw(7)  << std::right << afls_item.af().disk_id()       << ' '
-             << std::setfill(' ') << std::setw(12) << std::right << afls_item.af().size()          << ' '
-             << std::setfill(' ') << std::setw(13) << std::right << afls_item.af().cs().type()     << ' '
-             << std::setfill(' ') << std::setw(14) << std::right << afls_item.af().cs().value()    << ' '
-             << std::setfill(' ') << std::setw(16) << std::right << afls_item.af().storage_class() << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << afls_item.af().df().owner()    << ' '
-             << std::setfill(' ') << std::setw(8)  << std::right << afls_item.af().df().group()    << ' '
-             << std::setfill(' ') << std::setw(13) << std::right << afls_item.af().creation_time() << ' ';
-
-   if (afls_item.tf().superseded_by_vid().size()) {
-     std::cout << std::setfill(' ') << std::setw(7)  << std::right << afls_item.tf().superseded_by_vid()   << ' '
-               << std::setfill(' ') << std::setw(7)  << std::right << afls_item.tf().superseded_by_f_seq() << ' ';
-   } else {
-     std::cout << std::setfill(' ') << std::setw(7)  << std::right << "-" << ' '
-               << std::setfill(' ') << std::setw(7)  << std::right << "-" << ' ';
-   }
-   std::cout << afls_item.af().df().path()
-             << std::endl;
+  push_back(
+    afls_item.af().archive_id(),
+    afls_item.copy_nb(),
+    afls_item.tf().vid(),
+    afls_item.tf().f_seq(),
+    afls_item.tf().block_id(),
+    afls_item.af().disk_instance(),
+    afls_item.af().disk_id(),
+    afls_item.af().size(),
+    afls_item.af().cs().type(),
+    afls_item.af().cs().value(),
+    afls_item.af().storage_class(),
+    afls_item.af().df().owner(),
+    afls_item.af().df().group(),
+    afls_item.af().creation_time(),
+    afls_item.tf().superseded_by_vid().size() ? afls_item.tf().superseded_by_vid() : "-",
+    afls_item.tf().superseded_by_vid().size() ? std::to_string(afls_item.tf().superseded_by_f_seq()) : "-",
+    afls_item.af().df().path()
+  );
 }
 
-void CtaAdminTextFormatter::printAfLsSummaryHeader()
+void TextFormatter::printAfLsSummaryHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(13) << std::right << "total files" << ' '
@@ -83,14 +123,14 @@ void CtaAdminTextFormatter::printAfLsSummaryHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::ArchiveFileLsSummary &afls_summary)
+void TextFormatter::print(const cta::admin::ArchiveFileLsSummary &afls_summary)
 {
    std::cout << std::setfill(' ') << std::setw(13) << std::right << afls_summary.total_files() << ' '
              << std::setfill(' ') << std::setw(12) << std::right << afls_summary.total_size()  << ' '
              << std::endl;
 }
 
-void CtaAdminTextFormatter::printFrLsHeader()
+void TextFormatter::printFrLsHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(12) << std::right << "request type"   << ' '
@@ -102,7 +142,7 @@ void CtaAdminTextFormatter::printFrLsHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::FailedRequestLsItem &frls_item)
+void TextFormatter::print(const cta::admin::FailedRequestLsItem &frls_item)
 {
    std::string request_type;
    std::string tapepool_vid;
@@ -133,7 +173,7 @@ void CtaAdminTextFormatter::print(const cta::admin::FailedRequestLsItem &frls_it
    }
 }
 
-void CtaAdminTextFormatter::printFrLsSummaryHeader()
+void TextFormatter::printFrLsSummaryHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(12) << std::right << "request type"        << ' '
@@ -142,7 +182,7 @@ void CtaAdminTextFormatter::printFrLsSummaryHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::FailedRequestLsSummary &frls_summary)
+void TextFormatter::print(const cta::admin::FailedRequestLsSummary &frls_summary)
 {
    std::string request_type =
       frls_summary.request_type() == cta::admin::RequestType::ARCHIVE_REQUEST  ? "archive" :
@@ -154,7 +194,7 @@ void CtaAdminTextFormatter::print(const cta::admin::FailedRequestLsSummary &frls
              << std::endl;
 }
 
-void CtaAdminTextFormatter::printLpaHeader()
+void TextFormatter::printLpaHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(18) << std::right << "tapepool"       << ' '
@@ -172,7 +212,7 @@ void CtaAdminTextFormatter::printLpaHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::ListPendingArchivesItem &lpa_item)
+void TextFormatter::print(const cta::admin::ListPendingArchivesItem &lpa_item)
 {
    std::cout << std::setfill(' ') << std::setw(18) << std::right << lpa_item.tapepool()           << ' '
              << std::setfill(' ') << std::setw(11) << std::right << lpa_item.af().archive_id()    << ' '
@@ -189,7 +229,7 @@ void CtaAdminTextFormatter::print(const cta::admin::ListPendingArchivesItem &lpa
              << std::endl;
 }
 
-void CtaAdminTextFormatter::printLpaSummaryHeader()
+void TextFormatter::printLpaSummaryHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(18) << std::right << "tapepool"    << ' '
@@ -198,7 +238,7 @@ void CtaAdminTextFormatter::printLpaSummaryHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::ListPendingArchivesSummary &lpa_summary)
+void TextFormatter::print(const cta::admin::ListPendingArchivesSummary &lpa_summary)
 {
    std::cout << std::setfill(' ') << std::setw(18) << std::right << lpa_summary.tapepool()    << ' '
              << std::setfill(' ') << std::setw(13) << std::right << lpa_summary.total_files() << ' '
@@ -206,7 +246,7 @@ void CtaAdminTextFormatter::print(const cta::admin::ListPendingArchivesSummary &
              << std::endl;
 }
 
-void CtaAdminTextFormatter::printLprHeader()
+void TextFormatter::printLprHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(13) << std::right << "vid"        << ' '
@@ -221,7 +261,7 @@ void CtaAdminTextFormatter::printLprHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::ListPendingRetrievesItem &lpr_item)
+void TextFormatter::print(const cta::admin::ListPendingRetrievesItem &lpr_item)
 {
    std::cout << std::setfill(' ') << std::setw(13) << std::right << lpr_item.tf().vid()        << ' '
              << std::setfill(' ') << std::setw(11) << std::right << lpr_item.af().archive_id() << ' '
@@ -235,7 +275,7 @@ void CtaAdminTextFormatter::print(const cta::admin::ListPendingRetrievesItem &lp
              << std::endl;
 }
 
-void CtaAdminTextFormatter::printLprSummaryHeader()
+void TextFormatter::printLprSummaryHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(13) << std::right << "vid"         << ' '
@@ -244,7 +284,7 @@ void CtaAdminTextFormatter::printLprSummaryHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::ListPendingRetrievesSummary &lpr_summary)
+void TextFormatter::print(const cta::admin::ListPendingRetrievesSummary &lpr_summary)
 {
    std::cout << std::setfill(' ') << std::setw(13) << std::right << lpr_summary.vid()         << ' '
              << std::setfill(' ') << std::setw(13) << std::right << lpr_summary.total_files() << ' '
@@ -252,7 +292,7 @@ void CtaAdminTextFormatter::print(const cta::admin::ListPendingRetrievesSummary
              << std::endl;
 }
 
-void CtaAdminTextFormatter::printTpLsHeader()
+void TextFormatter::printTpLsHeader()
 {
    std::cout << TEXT_RED
              << std::setfill(' ') << std::setw(18) << std::right << "name"        << ' '
@@ -276,7 +316,7 @@ void CtaAdminTextFormatter::printTpLsHeader()
              << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::printTapeLsHeader(){
+void TextFormatter::printTapeLsHeader(){
   std::cout << TEXT_RED
             << std::setfill(' ') << std::setw(7) << std::right << "vid"              << ' '
             << std::setfill(' ') << std::setw(10) << std::right << "media type"       << ' '
@@ -307,7 +347,7 @@ void CtaAdminTextFormatter::printTapeLsHeader(){
 }
 
 
-void CtaAdminTextFormatter::print(const cta::admin::TapeLsItem &tals_item){
+void TextFormatter::print(const cta::admin::TapeLsItem &tals_item){
   std::cout << std::setfill(' ') << std::setw(7) << std::right << tals_item.vid()        << ' '
             << std::setfill(' ') << std::setw(10) << std::right << tals_item.media_type() << ' '
             << std::setfill(' ') << std::setw(7)  << std::right << tals_item.vendor()     << ' '
@@ -350,7 +390,7 @@ void CtaAdminTextFormatter::print(const cta::admin::TapeLsItem &tals_item){
               << std::endl;
 }
 
-void CtaAdminTextFormatter::printRepackLsHeader(){
+void TextFormatter::printRepackLsHeader(){
   std::cout << TEXT_RED
             << std::setfill(' ') << std::setw(7) << std::right << "vid"              << ' '
             << std::setfill(' ') << std::setw(50) << std::right << "repackBufferURL"       << ' '
@@ -370,7 +410,7 @@ void CtaAdminTextFormatter::printRepackLsHeader(){
             << TEXT_NORMAL << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::RepackLsItem &rels_item){
+void TextFormatter::print(const cta::admin::RepackLsItem &rels_item){
   std::cout << std::setfill(' ') << std::setw(7) << std::right << rels_item.vid()           << ' '
             << std::setfill(' ') << std::setw(50) << std::right << rels_item.repack_buffer_url()      << ' '
             << std::setfill(' ') << std::setw(17)  << std::right << rels_item.user_provided_files()           << ' '
@@ -388,7 +428,7 @@ void CtaAdminTextFormatter::print(const cta::admin::RepackLsItem &rels_item){
             << rels_item.status() << std::endl;
 }
 
-void CtaAdminTextFormatter::print(const cta::admin::TapePoolLsItem &tpls_item)
+void TextFormatter::print(const cta::admin::TapePoolLsItem &tpls_item)
 {
    std::string encrypt_str = tpls_item.encrypt() ? "true" : "false";
    uint64_t avail = tpls_item.capacity_bytes() > tpls_item.data_bytes() ?
diff --git a/cmdline/CtaAdminTextFormatter.hpp b/cmdline/CtaAdminTextFormatter.hpp
index 3c3c36b3ef..30d2fe0593 100644
--- a/cmdline/CtaAdminTextFormatter.hpp
+++ b/cmdline/CtaAdminTextFormatter.hpp
@@ -24,46 +24,89 @@
 namespace cta {
 namespace admin {
 
-class CtaAdminTextFormatter
+class TextFormatter
 {
 public:
-   // Output headers
-   static void printAfLsHeader();
-   static void printAfLsSummaryHeader();
-   static void printFrLsHeader();
-   static void printFrLsSummaryHeader();
-   static void printLpaHeader();
-   static void printLpaSummaryHeader();
-   static void printLprHeader();
-   static void printLprSummaryHeader();
-   static void printTpLsHeader();
-   static void printTapeLsHeader();
-   static void printRepackLsHeader();
+  /*!
+   * Constructor
+   *
+   * @param[in]  bufLines  Number of text lines to buffer before flushing formatted output
+   *                       (Not used for JSON output which does not need to be formatted
+   *                        so can be streamed directly)
+   */
+  TextFormatter(unsigned int bufLines = 1000) :
+    m_bufLines(bufLines) {
+    m_outputBuffer.reserve(bufLines);
+  }
+
+  ~TextFormatter() {
+    flush();
+  }
+
+  // Output headers
+  void printAfLsHeader();
+  static void printAfLsSummaryHeader();
+  static void printFrLsHeader();
+  static void printFrLsSummaryHeader();
+  static void printLpaHeader();
+  static void printLpaSummaryHeader();
+  static void printLprHeader();
+  static void printLprSummaryHeader();
+  static void printTpLsHeader();
+  static void printTapeLsHeader();
+  static void printRepackLsHeader();
    
-   // Output records
-   static void print(const ArchiveFileLsItem &afls_item);
-   static void print(const ArchiveFileLsSummary &afls_summary);
-   static void print(const FailedRequestLsItem &frls_item);
-   static void print(const FailedRequestLsSummary &frls_summary);
-   static void print(const ListPendingArchivesItem &lpa_item);
-   static void print(const ListPendingArchivesSummary &lpa_summary);
-   static void print(const ListPendingRetrievesItem &lpr_item);
-   static void print(const ListPendingRetrievesSummary &lpr_summary);
-   static void print(const TapePoolLsItem &tpls_item);
-   static void print(const TapeLsItem &tals_item);
-   static void print(const RepackLsItem &rels_item);
+  // Output records
+  void print(const ArchiveFileLsItem &afls_item);
+  static void print(const ArchiveFileLsSummary &afls_summary);
+  static void print(const FailedRequestLsItem &frls_item);
+  static void print(const FailedRequestLsSummary &frls_summary);
+  static void print(const ListPendingArchivesItem &lpa_item);
+  static void print(const ListPendingArchivesSummary &lpa_summary);
+  static void print(const ListPendingRetrievesItem &lpr_item);
+  static void print(const ListPendingRetrievesSummary &lpr_summary);
+  static void print(const TapePoolLsItem &tpls_item);
+  static void print(const TapeLsItem &tals_item);
+  static void print(const RepackLsItem &rels_item);
 
 private:
-   // Static method to convert time to string
-   static std::string timeToString(const time_t &time)
-   {
-      std::string timeString(ctime(&time));
-      timeString.resize(timeString.size()-1); //remove newline
-      return timeString;
-   }
+  //! Add a line to the buffer
+  template<typename... Args>
+  void push_back(Args... args) {
+    std::vector<std::string> line;
+    buildVector(line, args...);
+    m_outputBuffer.push_back(line);
+    if(m_outputBuffer.size() >= m_bufLines) flush();
+  }
+
+  //! Recursive variadic function to build a log string from an arbitrary number of items of arbitrary type
+  template<typename T, typename... Args>
+  void buildVector(std::vector<std::string> &line, const T &item, Args... args) {
+    buildVector(line, item);
+    buildVector(line, args...);
+  }
+
+  //! Base case function to add one item to the log
+  template<typename T>
+  void buildVector(std::vector<std::string> &line, const T &item) {
+    line.push_back(std::to_string(item));
+  }
+
+  void buildVector(std::vector<std::string> &line, const std::string &item) {
+    line.push_back(item);
+  }
+
+  void buildVector(std::vector<std::string> &line, const char *item) {
+    line.push_back(std::string(item));
+  }
+
+  void flush();                                            //!< Flush buffer to stdout
+
+  unsigned int m_bufLines;                                 //!< Number of text lines to buffer before flushing formatted output
+  std::vector<std::vector<std::string>> m_outputBuffer;    //!< Buffer for text output (not used for JSON)
 
-   static constexpr const char* const TEXT_RED    = "\x1b[31;1m";     //!< Terminal formatting code for red text
-   static constexpr const char* const TEXT_NORMAL = "\x1b[0m";        //!< Terminal formatting code for normal text
+  static const std::string TEXT_RED;                       //!< Terminal formatting code for red text
+  static const std::string TEXT_NORMAL;                    //!< Terminal formatting code for normal text
 };
 
 }}
-- 
GitLab