bagit/tag_definition.js

const { Context } = require('../core/context');
const { Util } = require('../core/util');

/**
 * This is a list of BagIt tags that the system sets internally
 * when it creates a bag.
 *
 * @type {string[]}
 */
const tagsSetBySystem = ['Bagging-Date', 'Bagging-Software',
                         'Payload-Oxum', 'DPN-Object-ID',
                         'First-Version-Object-ID', 'Bag-Size',
                         'BagIt-Profile-Identifier'];

/**
 * TagDefinition describes the name of a tag, which tag file it should
 * appear in, what its allowed values are, and more.
 *
 * TagDefinitions belong to the BagItProfile object.
 *
 * @see {@link BagItProfile}
 *
 * @param {object} opts - Values to copy into TagDefinition properties.
 * The properties of this object match the properties of the TagDefinition
 * class, which are described below.
 *
 */
class TagDefinition {
    constructor(opts = {}) {
        /**
          * The unique identifier for this TagDefinition.
          * This is a version 4 UUID in hex string format.
          *
          * @type {string}
          */
        this.id = Util.uuid4();
        /**
          * The name of the tag file in which this tag is
          * expected to appear.
          *
          * @type {string}
          */
        this.tagFile = opts.tagFile;
        /**
          * The name of the tag. For example, 'Source-Organization'.
          *
          * @type {string}
          */
        this.tagName = opts.tagName;
        /**
          * True if this tag requires a value.
          *
          * @type {boolean}
          * @default false
          */
        this.required = opts.required === true ? true : false;
        /**
          * A list of valid values for this tag. If this list
          * is empty, then any values are valid. If it is not
          * empty, the BagIt validator will check to ensure that
          * any values assigned to this tag are in the list. If
          * they're not, the validator will reject the bag as invalid.
          *
          * @type {string[]}
          */
        this.values = opts.values || [];
        /**
          * The default value for this tag. This is the value
          * that will be assigned to the tag when you create a bag
          * and don't specify a value for this tag. For example,
          * if you set the default value of the 'Source-Organization'
          * tag, you'll never have to set that value again when
          * building individual bags (unless you want to). The bagger
          * will simply fill in the default. The userValue property,
          * if non-empty, will override defaultValue during the bagging
          * process.
          *
          * @see {@link userValue}
          *
          * @type {string}
          */
        this.defaultValue = opts.defaultValue || "";
        /**
          * A user-defined value to assign to this tag when creating
          * a bag. If this value is non-empty, it overrides defaultValue.
          * You will always want to set this on a per-bag basis for certain
          * tags, such as 'Title', 'Description', or any other bag-specific
          * tag.
          *
          * @see {@link defaultValue}
          *
          * @type {string}
          */
        this.userValue = opts.userValue || "";
        /**
          * Help describes the significance of this tag to end users.
          * In the DART UI, it appears as a tooltip to help the user fill
          * in a meaningful value.
          *
          * @type {string}
          */
        this.help = opts.help || "";
        /**
          * The isBuiltIn flag is true for built-in tags whose
          * definition should not be altered or even changeable
          * by the user. For example, the BagIt-Version and
          * Tag-File-Character-Encoding tags in the bagit.txt file
          * are builtins whose value the user can edit, and whose
          * definition they cannot alter.
          *
          * @type {boolean}
          */
        this.isBuiltIn = opts.isBuiltIn === true ? true : false;
        /**
          * isUserAddedFile is a special flag indicating the user
          * added this custom tag file, which is not part of the
          * standard BagIt profile. The DART UI handles these files
          * differently from those that are part of a core BagItProfile.
          *
          * @type {boolean}
          */
        this.isUserAddedFile = opts.isUserAddedFile === true ? true : false;
        /**
          * isUserAddedTag describes whether this tag was added by
          * the user (and hence is not part of the standard BagIt profile
          * upon which the bag is built). Users may add custom tags to
          * standard tag files (such as bag-info.txt) or to their own
          * custom tag files. All tags in custom tag files will have
          * isUserAddedTag = true.
          *
          * @type {boolean}
          */
        this.isUserAddedTag = opts.isUserAddedTag === true ? true : false;
        /**
         * wasAddedForJob will be true if the user added this custom tag
         * from the jobs UI. This means it's a job-specific tag and the
         * user should be able to delete it without breaking the bag's
         * conformity to a profile.
         */
        this.wasAddedForJob = opts.wasAddedForJob === true ? true : false;
        /**
         * Contains information describing validation errors. Key is the
         * name of the invalid field. Value is a description of why the
         * field is not valid.
         *
         * @type {Object<string, string>}
         */
        this.errors = opts.errors || {};
    }

    /**
     * validate returns true or false, indicating whether this object
     * contains complete and valid data. If it returns false, check
     * the errors property for specific errors.
     *
     * @returns {boolean}
     */
    validate() {
        this.errors = {};
        if (Util.isEmpty(this.tagFile)) {
            this.errors['tagFile'] = "You must specify a tag file.";
        }
        if (Util.isEmpty(this.tagName)) {
            this.errors['tagName'] = "You must specify a tag name.";
        }
        if (!Util.isEmptyStringArray(this.values)) {
            if (!Util.isEmpty(this.defaultValue) && !Util.listContains(this.values, this.defaultValue)) {
            this.errors['defaultValue'] = "The default value must be one of the allowed values.";
            }
            if (!Util.isEmpty(this.userValue) && !Util.listContains(this.values, this.userValue)) {
                this.errors['userValue'] = "The value must be one of the allowed values.";
            }
        }
        return Object.keys(this.errors).length === 0;
    }

    /**
     * This returns a list of errors describing what's wrong with
     * the tag's value. For example, if a tag value is empty when empty
     * values are not allowed, or if the value is not in the tag's list
     * of valid values.
     *
     * @returns {string[]} - A list of errors.
     */
    validateForJob() {
        this.errors = {}
        // If the system must set this value, don't try to validate
        // it in the Job UI, where it will have to be empty. The
        // bagger will set it later, and we'll validate it after bagging.
        if (this.systemMustSet()) {
            return true;
        }
        var value = this.getValue();
        if (this.required && Util.isEmpty(value)) {
            this.errors['userValue'] = Context.y18n.__("This tag requires a value.");
        } else if (this.values.length > 0 && !Util.listContains(this.values, value)) {
            this.errors['userValue'] = Context.y18n.__("The value is not in the list of allowed values.");
        }
        return Object.keys(this.errors).length === 0;
    }

    /**
      * Returns true if the system, and not the user, must set this value.
      * The system sets certain values, such as Bagging-Date, internally
      * when it creates the bag.
      *
      * @see {@link tagsSetBySystem}
      *
      * @returns {string[]}
      */
    systemMustSet() {
        return Util.listContains(tagsSetBySystem, this.tagName);
    }

    /**
      * Returns this tag's userValue, if that's non-empty, or its defaultValue.
      *
      * @returns {string}
      */
    getValue() {
        return this.userValue || this.defaultValue;
    }

    /**
      * Returns true if the tag name contains the word 'description'.
      *
      * @returns {boolean}
      */
    looksLikeDescriptionTag() {
        return this.tagName.toLowerCase().includes("description");
    }

    /**
      * toFormattedString returns the tag as string in a format suitable
      * for writing to a tag file. Following LOC's bagit.py, this function
      * does not break lines into 79 character chunks. It prints the whole
      * tag on a single line, replacing newlines with spaces.
      *
      * @returns {string}
      */
    toFormattedString() {
        var val = (this.getValue() || "").replace(/(\r\n)|\n|\r/g, ' ').replace(/ +/g, ' ').trim();
        return `${this.tagName}: ${val}`;
    }

    /**
      * Given a tag string in command-line format, this returns a new
      * TagDefinition object.
      *
      * @param {string} str - A tag definition string in command-line format,
      * which is filename/tagname: value. For example, call with param
      * 'bag-info/Source-Organization: Faber College' would return a new
      * TagDefinition object with the properties tagFile = 'bag-info.txt',
      * tagName = 'Souce-Organization', userValue = 'Faber College'.
      *
      * @returns {TagDefinition}
      */
    static fromCommandLineArg(str) {
        var tag;
        try {
            var i = str.indexOf(':');
            var [fileAndTag, value] = [str.slice(0,i), str.slice(i+1)];
            var [file, tagName] = fileAndTag.split('/');
            if (!file.endsWith('.txt')) {
                file += '.txt';
            }
            var tag = new TagDefinition({
                tagFile: file.trim(),
                tagName: tagName.trim()
            });
            tag.userValue = value.trim();
            return tag;
        } catch (ex) {
            throw `Invalid format for command-line tag string. '${str}' -> sould be in format 'filename/tagname: value'`
        }
    }
}

module.exports.TagDefinition = TagDefinition;