Source: model/term-converter.js

const N3 = require('n3');
const {DataFactory} = N3;
const {namedNode, literal, quad, blankNode, variable} = DataFactory;
const StringUtils = require('../util/string-utils');
const base64url = require('base64url');
const ENCODED_RDFSTAR_TRIPLE_PREFIX = 'urn:rdf4j:triple:';

/**
 * Utility class for converting strings to terms, terms to quads and
 * quads to string according to the {@link https://rdf.js.org} specification.
 *
 * @class
 * @author Mihail Radkov
 * @author Svilen Velikov
 * @author Teodossi Dossev
 */
class TermConverter {
  /**
   * Convert the supplied params to a collection of quads.
   *
   * The produced quads size depends on the supplied amount of context.
   *
   * @public
   * @static
   * @param {string} subject the quad's subject
   * @param {string} predicate the quad's predicate
   * @param {string} object the quad's object
   * @param {(string|string[])} [contexts] the quad's context
   * @return {Quad[]} a collection of quads constructed from the provided params
   */
  static getQuads(subject, predicate, object, contexts) {
    const objectTerm = TermConverter.toObject(object);
    return TermConverter.toQuads(subject, predicate, objectTerm, contexts);
  }

  /**
   * Convert the supplied params to a collection of quads.
   *
   * The quads object term will be a literal with a data type or a language.
   *
   * The produced quads size depends on the supplied amount of context.
   *
   * @public
   * @static
   * @param {string} subject the quad's subject
   * @param {string} predicate the quad's predicate
   * @param {string} object the quad's object
   * @param {(string|string[])} [contexts] the quad's context
   * @param {string} type the quad's data type
   * @param {string} language the quad's literal language
   * @return {Quad[]} a collection of quads constructed from the provided params
   */
  static getLiteralQuads(subject, predicate, object, contexts, type, language) {
    let objectTerm;
    if (language) {
      objectTerm = TermConverter.toObjectWithLanguage(object, language);
    } else {
      objectTerm = TermConverter.toObjectWithDataType(object, type);
    }
    return TermConverter.toQuads(subject, predicate, objectTerm, contexts);
  }

  /**
   * Convert the supplied params to terms and then to a collection of quads.
   * The supplied object should already be converted to a term.
   *
   * The produced quads size depends on the supplied amount of context.
   *
   * @private
   * @static
   * @param {string} subject the quad's subject
   * @param {string} predicate the quad's predicate
   * @param {Term} objectTerm the quads object already converted to a Term
   * @param {(string|string[])} contexts the quad's context
   * @return {Quad[]} collection of quads constructed from the provided params
   */
  static toQuads(subject, predicate, objectTerm, contexts) {
    const subjectTerm = TermConverter.toSubject(subject);
    const predicateTerm = TermConverter.toPredicate(predicate);
    const contextTerms = TermConverter.toGraphs(contexts);

    if (contextTerms && contextTerms.length) {
      return contextTerms.map((graph) => quad(subjectTerm, predicateTerm,
        objectTerm, graph));
    }
    return [quad(subjectTerm, predicateTerm, objectTerm)];
  }

  /**
   * Serializes the provided collection of quads to Turtle format or Trig in
   * case any of the quads have context.
   *
   * @public
   * @static
   * @param {Quad[]} quads the collection of quads to serialize to Turtle
   * @return {string} a promise that will be resolved to Turtle or Trig
   * text or rejected if the quads cannot be serialized
   */
  static toString(quads) {
    const writer = TermConverter.getWriter();
    writer.addQuads(quads);

    let converted = '';
    writer.end((error, result) => {
      if (error) {
        throw new Error(error);
      } else {
        converted = result.trim();
      }
    });
    return converted;
  }

  /**
   * Converts the provided value to N-Triple encoded value in case it is not
   * already one or a literal value.
   *
   * For example:
   * <ul>
   *   <li><i>http://resource</i> encodes to <i><http://resource></i></li>
   *   <li><i>"Literal title"@en</i> will not be encoded</li>
   *   <li><i><http://resource></i> encodes to the same value</li>
   * </ul>
   *
   * Empty or null values are ignored.
   *
   * @public
   * @static
   * @param {string} value the value for converting
   * @return {string} the converted value to N-Triple
   */
  static toNTripleValue(value) {
    if (StringUtils.isNotBlank(value)) {
      if (value.startsWith('"')) {
        // Do not convert literals
        return value;
      }
      if (value.startsWith('<')) {
        // Value is probably already encoded as N-Triple
        return value;
      }
      return `<${value}>`;
    }
  }

  /**
   * Converts the provided values to N-Triple encoded values in case they are
   * not already one or literal values.
   *
   * Empty or null values are ignored.
   *
   * @see {@link #toNTripleValue}

   * @public
   * @static
   * @param {string|string[]} values the values for converting
   * @return {string|string[]} the converted value or values to N-Triple
   */
  static toNTripleValues(values) {
    if (values instanceof Array) {
      return values
        .filter((value) => StringUtils.isNotBlank(value))
        .map((value) => TermConverter.toNTripleValue(value));
    }
    return TermConverter.toNTripleValue(values);
  }

  /**
   * Converts the provided subject string to a specific Term based on the value.
   *
   * @private
   * @param {string} value the subject to convert
   * @return {BlankNode|Variable|NamedNode} the provided subject as Term
   */
  static toSubject(value) {
    return TermConverter.toTerm(value);
  }

  /**
   * Converts the provided predicate string to a specific Term based on the
   * value.
   *
   * @private
   * @param {string} value the predicate to convert
   * @return {Variable|NamedNode} the provided predicate as Term
   */
  static toPredicate(value) {
    if (TermConverter.isVariable(value)) {
      return TermConverter.toVariable(value);
    }
    return namedNode(value);
  }

  /**
   * Converts the provided object string to a specific Term based on the value.
   *
   * This is not handling literal strings. For that use
   * {@link TermConverter#toObjectWithLanguage} or
   * {@link TermConverter#toObjectWithDataType}
   *
   * @private
   * @param {string} value the object to convert
   * @return {BlankNode|Variable|NamedNode} the provided object as Term
   */
  static toObject(value) {
    // Same as subject (when it is not literal)
    return TermConverter.toSubject(value);
  }

  /**
   * Converts the provided object and language to a Literal term.
   *
   * @private
   * @param {string} object the value to convert
   * @param {string} language the object's language
   * @return {Literal} the provided object as Literal
   */
  static toObjectWithLanguage(object, language) {
    return literal(object, language);
  }

  /**
   * Converts the provided object and data type to a Literal term.
   *
   * @private
   * @param {string} object the value to convert
   * @param {string} dataType the object's type
   * @return {Literal} the provided object as Literal
   */
  static toObjectWithDataType(object, dataType) {
    return literal(object, namedNode(dataType));
  }

  /**
   * Converts the provided context to a collection of Term.
   *
   * The produced terms size depends on the supplied amount of context.
   *
   * @private
   * @param {string|string[]} [contexts] the contexts to convert
   * @return {Term[]} the provided contexts as Terms
   */
  static toGraphs(contexts) {
    if (!contexts || (contexts.length && contexts.length < 1)) {
      return [];
    }
    // Convert to array
    if (!(contexts instanceof Array)) {
      contexts = [contexts];
    }
    // Convert to terms
    return contexts.map((context) => TermConverter.toTerm(context));
  }

  /**
   * Converts the provided string to a specific Term based on the value.
   *
   * <ul>
   *  <li>If the string begins with <code>_:</code> it will be converted to a
   *  blank node term.</li>
   *  <li>If the string begins with <code>?</code> it will be converted to a
   *  variable term.</li>
   *  <li>Otherwise it will be converted a simple named node term.</li>
   * </ul>
   *
   * @private
   * @param {string} value the string to convert
   * @return {BlankNode|Variable|NamedNode} the provided value as Term
   */
  static toTerm(value) {
    if (TermConverter.isBlankNode(value)) {
      // Trim leading _:
      return blankNode(value.substring(2));
    }
    if (TermConverter.isVariable(value)) {
      return TermConverter.toVariable(value);
    }
    return namedNode(value);
  }

  /**
   * Returns a variable term from the provided value without leading ?
   *
   * @private
   * @param {string} value the value to convert to variable
   * @return {Variable} the produced variable
   */
  static toVariable(value) {
    // Trim leading ?
    return variable(value.substring(1));
  }

  /**
   * Checks if the provided value is a blank node.
   *
   * Blank nodes are such values that start with <code>_:</code> prefix
   *
   * @private
   * @param {string} value the value to check
   * @return {boolean} <code>true</code> if the value is a blank node
   *                    or <code>false</code> otherwise
   */
  static isBlankNode(value) {
    return value.startsWith('_:');
  }

  /**
   * Checks if the provided value is a variable.
   *
   * Variables are such values that start with <code>?</code> prefix
   *
   * @private
   * @param {string} value the value to check
   * @return {boolean} <code>true</code> if the value is a variable
   *                    or <code>false</code> otherwise
   */
  static isVariable(value) {
    return value.startsWith('?');
  }

  /**
   * Instantiates new N3 writer for quads.
   *
   * This writer is not reusable, after invoking <code>end()</code> it won't
   * allow further quads insertions.
   *
   * @private
   * @return {N3.Writer} new writer for quads
   */
  static getWriter() {
    return new N3.Writer();
  }

  /**
   * Decodes from Base64 encoded RDFStar triple.
   *
   * @param {string} encodedTriple to be decoded from base64 url string
   * @return {string} decoded RDFStar triple, returns unchanged if the provided
   * triple is not in the expected format
   */
  static fromBase64RdfStarTriple(encodedTriple) {
    if (encodedTriple.startsWith(ENCODED_RDFSTAR_TRIPLE_PREFIX)) {
      return base64url
        .decode(encodedTriple.slice(ENCODED_RDFSTAR_TRIPLE_PREFIX.length));
    }
    return encodedTriple;
  }

  /**
   * Encodes RDFStarTriple to Base64 string.
   *
   * @param {string} triple to be encoded as base64 url string
   * @return {string} encoded RDFStart triple, returns unchanged if the provided
   * triple is not in the expected format
   */
  static toBase64RdfStarTriple(triple) {
    if (triple.startsWith('<<') && triple.endsWith('>>')) {
      return ENCODED_RDFSTAR_TRIPLE_PREFIX + base64url.encode(triple);
    }
    return triple;
  }
}

module.exports = TermConverter;