Split large XML or text files into multiple smaller files in Adobe Campaign 7 when source files contain too many lines.

This solution provides a reusable JavaScript library that splits files based on configurable occurrence patterns, header/footer templates, and max occurrences per file.

Preparation

You receive XML files that contain too many lines, you need to split them in multiple smaller files.

Original file:

<header>
    <row></row>
    <row></row>
    <row></row>
    <row></row>
    <row></row>
</footer>

Will be split into 3 files with occurence="<row>" and maxOccurencesPerFile=2:

<!-- file_0001.xml -->
<header>
  <row></row> <!-- occurence 1 -->
  <row></row> <!-- occurence 2 -->
</footer>
<!-- file_0002.xml -->
<header>
  <row></row> <!-- occurence 3 -->
  <row></row> <!-- occurence 4 -->
</footer>
<!-- file_0003.xml -->
<header>
  <row></row> <!-- occurence 5 -->
</footer>

Walkthrough

Usage

loadLibrary("nms:fileSplitter");

var options = {
  filename: vars.filename,
  outputDir: vars.dir,
  header:
    '<?xml version="1.0" encoding="UTF-8"?>\n<orders xmlns="http://www.demandware.com/xml/impex/customer/2006-10-31">',
  occurence: "<order order-no",
  footer: "</orders>",
  ommitFirstHeader: true,
  maxOccurencesPerFile: 50,
  getNewFile: function (originalFile, nbOfFilesDone, options) {
    var actualFile =
      options.outputDir +
      "/" +
      originalFile.name.replace(".xml.txt", "") +
      "_" +
      nbOfFilesDone.toString().padStart(4, "0") +
      ".xml";
    var f = new File(actualFile);
    f.open("a");
    logInfo("Created file " + f.fullName);
    return f;
  },
};
splitFile(options);

Source code

Create a JavaScript file nms:fileSplitter as:

/**
 * Splits a file such as:
 * A
 * A
 * A
 * into:
 * File 1:
 * header
 * A
 * A
 * footer
 * File 2:
 * header
 * A
 * footer
 *
 * with params occurence="A", times=2
 */
loadLibrary("vendor:underscore"); // @see https://myrosblog.com/adobe-campaign/connect-npm-library

const TAG = "nms:fileSplitter | ";

function splitFile(options) {
  var defaultOptions = {
    filename: null,
    outputDir: null,
    header: null,
    occurence: null,
    footer: null,
    getNewFile: null,

    ommitFirstHeader: true,
    maxOccurencesPerFile: 10,
  };
  options = _.defaults(options, defaultOptions);
  if (
    !options.filename ||
    !options.outputDir ||
    !options.header ||
    !options.occurence ||
    !options.footer ||
    !options.getNewFile
  ) {
    return logError(
      TAG +
        "splitFile error, missing filename, outputDir, header, occurence, footer or getNewFile in options"
    );
  }
  logInfo(TAG + "splitFile with options:");
  logInfo(TAG + JSON.stringify(options));

  var originalFile = new File(options.filename);

  logInfo(TAG + "loading: " + options.filename);
  var text = loadFile(options.filename);
  logInfo(TAG + "splitting by newline");
  var lines = text.split("\n");
  logInfo(TAG + "ready to split into files");

  var textToWrite = "";
  if (!options.ommitFirstHeader) {
    textToWrite = options.header;
  }
  var nbOfFilesDone = 1;
  var actualTime = 0;

  var f = options.getNewFile(originalFile, nbOfFilesDone, options);

  for (var i in lines) {
    var line = lines[i];
    // if we find an occurence
    if (line.indexOf(options.occurence) > -1) {
      // write
      f.writeln(textToWrite);
      textToWrite = "";
      // if it's time to create a new file
      if (actualTime >= options.maxOccurencesPerFile) {
        logInfo(
          TAG +
            "New file for occurence at line " +
            i +
            " for occurence(" +
            actualTime +
            "/" +
            options.maxOccurencesPerFile +
            ") for line (" +
            i +
            ")"
        );
        // write footer and close file
        f.writeln(options.footer);
        f.close();
        // reset vars
        actualTime = 0;
        nbOfFilesDone++;
        // open the next file
        f = options.getNewFile(originalFile, nbOfFilesDone, options);
        /// adding header
        if (nbOfFilesDone > 1 || !options.ommitFirstHeader) {
          f.writeln(options.header);
        }
      }
      actualTime++;
    }

    if (i > 0 && line.length > 1) {
      textToWrite += "\n";
    }
    textToWrite += line;
  }
  // last one
  f.writeln(textToWrite);
  f.close();

  logInfo(TAG + "finished");
  return;
}

Validation & troubleshooting

NB: to include underscoreJS library, see how to use a JavaScript library in Adobe Campaign/

Going further

Polyfills for padStart() in Adobe Campaign

Note: to make it work with Adobe Campaign, add the following polyfills for padStart() in nms:polyfill:

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
if (!String.prototype.repeat) {
  String.prototype.repeat = function (count) {
    "use strict";
    if (this == null) {
      throw new TypeError("can't convert " + this + " to object");
    }
    var str = "" + this;
    count = +count;
    if (count != count) {
      count = 0;
    }
    if (count < 0) {
      throw new RangeError("repeat count must be non-negative");
    }
    if (count == Infinity) {
      throw new RangeError("repeat count must be less than infinity");
    }
    count = Math.floor(count);
    if (str.length == 0 || count == 0) {
      return "";
    }
    // Ensuring count is a 31-bit integer allows us to heavily optimize the
    // main part. But anyway, most current (August 2014) browsers can't handle
    // strings 1 << 28 chars or longer, so:
    if (str.length * count >= 1 << 28) {
      throw new RangeError("repeat count must not overflow maximum string size");
    }
    var rpt = "";
    for (var i = 0; i < count; i++) {
      rpt += str;
    }
    return rpt;
  };
}
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
if (!String.prototype.padStart) {
  String.prototype.padStart = function padStart(targetLength, padString) {
    targetLength = targetLength >> 0; //truncate if number or convert non-number to 0;
    padString = String(typeof padString !== "undefined" ? padString : " ");
    if (this.length > targetLength) {
      return String(this);
    } else {
      targetLength = targetLength - this.length;
      if (targetLength > padString.length) {
        padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
      }
      return padString.slice(0, targetLength) + String(this);
    }
  };
}

Then add loadLibrary('nms:polyfill'); in your main file.