services/n3FileService.js

const fs = require('fs')
const path = require('path')

const N3 = require('n3')
const N3Parser = require('n3-parser.js').N3Parser

/**
 * Handles all logic when dealing with n3 files or strings.
 */
class N3FileService {
  /**
    * The constructor initializes our N3 parsers and sets the location for storing temporary files
    */
  constructor () {
    this.parser = new N3.Parser()
    this.parserN3 = new N3Parser()
    this.tempFolder = 'data/temp/'
    this.extensions = ['.ttl', '.n3', '.rdf']
  }

  /**
    * Checks if file is an RDF file ('.ttl','.n3','.rdf').
    * @param {String} file - Path to the file
    * @returns {Boolean} - Returns true if the file is an RDF-file.
    */
  isRDFFile (file) {
    const current = path.extname(file)
    if (this.extensions.indexOf(current) !== -1) {
      return true
    }
    return false
  }

  /**
     * Creates a new file or opens it if it already exists
     *
     * !!File handle must be closed or it might leak
     *
     * Uses NodeJS's `fs.promises.open`.
     *
     * @param {String} path - Path to the file were trying to create
     * @returns {Promise} - Promise that resolves to the file handle of the file
     */
  async newFile (path) {
    // if file doesn't already exist, create the file

    return fs.promises.open(path)
  }

  /**
     * Deletes all content from a file
     * @param {String} path - Path to the file
     * @returns {Promise}
     */
  async deleteContent (file) {
    return fs.promises.truncate(file, 0)
      .then(() => console.log('File ', file, ' truncated to 0'))
  }

  /**
    * Convert contents of n3 file to an array of usable quad objects.
    * @param {String} file - Name of the file we're trying to parse
    * @returns {Array} - Array of Quads.
    */
  async fileToQuads (file) {
    // read file contents
    // var content = fs.readFileSync(file, 'utf8');

    // todo: check if file is proper RDF file

    // parse the content to quads and return
    try {
      const content = await fs.promises.readFile(file, 'utf8')
      return this.parser.parse(content)
    } catch (e) {
      fs.promises.writeFile(file, '', 'utf-8')
      return this.parser.parse('')
    }
  }

  /**
    * Reads the contents of a file
    * @param {String} file - Name of the file we're trying to parse
    * @returns {String} - Contents of file
    */
  async fileToString (file) {
    // is rdf file
    if (!this.isRDFFile(file)) { return '' }

    // read file contents
    const content = await fs.promises.readFile(file, 'utf8')
    return content
  }

  /**
    * Convert contents of an N3-string to an array of usable quad objects.
    * @param {String} n3string - N3-string to parse
    * @param {Array} - Array of Quads
    */
  stringToQuads (n3string) {
    // parse the content to quads and return
    return this.parser.parse(n3string)
  }

  /**
    * Converts array of quad objects to an n3string
    * @param {Array} quads - Quads to convert
    * @param {Object} prefixes - Object containing all necessary prefixes as key-value pairs
    * @returns {String} - Equivalent N3-string of the input quads
    */
  async quadsToString (quads, prefixes) {
    return new Promise((resolve, reject) => {
      // initialize writer
      const writer = new N3.Writer({ prefixes: prefixes })

      // add each of the quads to the writer
      writer.addQuads(quads)

      // close the writer and send the result
      writer.end((error, result) => {
        if (error) {
          reject(error)
        }
        resolve(result)
      })
    })
  }

  /**
    * Convert contents of n3 string to a JSONLD string
    * @param {String} n3string
    * @returns {String} Equivalent JSONLD of the input
    */
  stringToJSONLD (n3string) {
    return this.parserN3.toJSONLD(n3string)
  }

  /**
    * Writes an n3string to an n3 file.
    * This will overwrite the file losing any previous contents
    * @param {String} file - Name of the file were writing to
    * @param {String} n3string - New contents of the file
    * @returns {Promise}
    */
  async writeStringToFile (file, n3string) {
    // creates file if it doesn't exist
    return fs.promises.writeFile(file, n3string)
  }

  /**
    * Writes an array of quads to an n3 file.
    * This will overwrite the file losing any previous contents
    * @param {String} file - Name of the file were writing to
    * @param {Array} quads - List of Quads to be written
    * @param {Object} prefixes - Object containing all necessary prefixes as key-value pairs
    * @returns {Promise}
    */
  async writeQuadsToFile (file, quads, prefixes) {
    return new Promise(
      (resolve, reject) => {
        // open a writable stream
        const access = fs.createWriteStream(file)

        // todo: check if file is proper RDF file
        // initialize N3 writer to write to our stream
        const writer = new N3.Writer(access, { prefixes: prefixes })
        writer.addQuads(quads)
        writer.end((error, result) => {
          // close the writer and resolve the result
          access.end()
          if (error) {
            reject(error)
          } else {
            resolve(result)
          }
        })
      })
  }

  /**
    * Retrieves the prefixes from an n3 string, returns prefixes as json
    * @param {String} n3string
    * @returns {Object} - Prefixes as key-value pairs
    */
  prefixesFromString (n3string) {
    // Use RegExp to extract all prefixes
    const result = n3string.match(/@prefix.*\./g)

    // process the prefixes to json format, this way it will be readily usable by the N3 package
    const prefixes = {}
    for (const prefixKey in result) {
      const prefix = result[prefixKey].match(/@prefix (.*): <(.*)>\./)
      prefixes[prefix[1]] = prefix[2]
    }
    return prefixes
  }

  /**
    * Retrieves the prefixes from a file as json.
    * @param {String} file - Name of the file to be read.
    * @returns {Object} - Prefixes as key-value pairs
    */
  async prefixesFromFile (file) {
    // first get n3string from file
    const n3string = await this.fileToString(file)

    return this.prefixesFromString(n3string)
  }

  /**
    * Retrieve prefixes from n3string as string.
    * @param {String} n3string
    * @returns {String} Single string containing all prefixes
    */
  prefixesFromStringAsString (n3string) {
    // Use RegExp to extract all prefixes as array
    const result = n3string.match(/@prefix.*\./g)

    // convert to  single string
    return result.join('\r\n')
  }

  /**
    * Adds an N3-string to existing file
    * @param {String} file - Path to the file were appending to
    * @param {String} n3string - N3-string to be added to the file
    * @returns {Promise} Promise that resolves when the I/O has finished
    */
  async appendN3stringToFile (file, n3string) {
    // combine the quads from the file and n3 string
    let quads = await this.fileToQuads(file)
    quads = quads.concat(this.stringToQuads(n3string))
    // retrieve prefixes from the n3 string
    const prefixes = this.prefixesFromString(n3string)
    return this.writeQuadsToFile(file, quads, prefixes)
  }

  /**
    * Generate a temporary file and write the specified quads to the file
    *
    * !!Needs to be manually unlinked after use
    * @param {Array} quads - Quads to be written
    * @param {Object} prefixes - Object containing all necessary prefixes as key-value pairs
    * @returns {String} the filepath relative to the server root
    */
  async quadsToTempFile (quads, prefixes) {
    // generate random string
    const r = Math.random().toString(36).substr(2, 5)

    // define the file we want to write to
    var fileName = r + '.n3'
    var file = this.tempFolder + fileName

    // create new temp file
    // await this.newFile(file).then(fi);
    await this.writeQuadsToFile(file, quads, prefixes)

    return file
  }
}

module.exports = N3FileService