Newer
Older
li-utils / index.js
/* global module */
/* jslint node: true */
/* jslint indent: 2 */
'use strict';

/* Module Require */
var dateFormat = require('dateformat'),
  cheerio = require('cheerio'),
  mkdirp = require('mkdirp'),
  mustache = require('mustache'),
  request = require('request'),
  fs = require('fs'),
  path = require('path'),
  extend = require('util')._extend,
  child_process = require('child_process');

/* Constantes */
var JSON_EXTENSION = new RegExp(/(.json)$/g);

// Main Object
var object = {};

// Regroupe les fonctions liées aux chemins
object.paths = {};

/**
 * Mustache est utilisé pour rendre le TEI standOff.
 * La fonction d'échappement doit s'appliquer aux 5 caractères XML réservés
 * @param {*} text
 * @returns XML-escaped test
 */
mustache.escape = function(text) {
  return ('' + text).replace(/[<>&'"]/g, function (c) {
      switch (c) {
          case '<': return '&lt;';
          case '>': return '&gt;';
          case '&': return '&amp;';
          case '\'': return '&apos;';
          case '"': return '&quot;';
      }
  });
}

/**
 * Initialise les chemins d'un module R&D
 * @param {object} paths Liste des chemins sous forme d'objet JSON (clé => valeur)
 * @param {string} root Racine du module
 * @return {object} L'objet contenant les chemins initialisés
 */
object.paths.init = function(paths, root) {
  var result = {};
  // Pour chaque chemin
  for (var k in paths) {
    // On construit le chemin absolu
    if (paths.hasOwnProperty(k) && typeof paths[k] !== 'function') {
      result[k] = path.join(root, paths[k]);
    }
  }
  return result;
};

// Regroupe les fonctions liées aux ressources
object.resources = {};

/**
 * Require toutes les ressources d'un module R&D
 * @param {object} paths Liste des chemins sous forme d'objet JSON (clé => valeur)
 * @return {object} L'objet contenant toutes les ressources chargée
 */
object.resources.require = function(paths) {
  var result = {};
  for (var k in paths) {
    if ((typeof paths[k] === 'string') && paths[k].match(JSON_EXTENSION)) { // Require du fichier s'il a une extension JSON
      result[k] = require(paths[k]);
    } else if (typeof paths[k] === 'object') { // Relance du traitement si un c'est un object
      result[k] = object.resources.require(paths[k]);
    }
  }
  return result;
};

// Regroupe les fonctions liées aux fichiers dans la chaine LoadIstex
object.files = {};

/**
 * Retourne les objets du Tableau de fichier respectant au moins un des "ensemble de critères" spécifiées
 * Exemple : Je souhaite récupérer le fichier txt généré par LoadIstex ou un fichier txt
 *   files = docObject.fulltext (paramètre du docObject contenant les infos liées au fulltext)
 *   options = [
 *     { mime: 'text/plain', original: false }, --> ficher txt généré par LoadIstex (original = false)
 *     { mime: 'text/plain'}                    --> ficher txt
 *   ]
 * @param {array} files (jsonLine.metadata || jsonLine.fulltext)
 * @param {array} options Liste (ordonnées) des caractéristiques du document recherché
 * @return {array} L'objet correspondant le mieux aux critères ou []
 */
object.files.selectAll = function(files, options) {
  var result = [],
    _files = extend([], files); // copy du Tableau de fichier
  for (var x = 0; x < options.length; x++) {
    var keys = Object.keys(options[x]);
    while (_files.length > 0) {
      var found = true,
        file = _files.shift();
      for (var i = 0; i < keys.length; i++) {
        found &= (options[x][keys[i]] instanceof RegExp) ? options[x][keys[i]].test(file[keys[i]]) : (file[keys[i]] === options[x][keys[i]]);
        if (!found) break;
      }
      if (found) {
        result.push(file);
      }
    }
  }
  return result;
};

/**
 * Retourne le premier objet du Tableau de fichier respectant l'un des "ensemble de critères" spécifiées
 * Exemple : Je souhaite récupérer le fichier txt généré par LoadIstex ou un fichier txt
 *   files = docObject.fulltext (paramètre du docObject contenant les infos liées au fulltext)
 *   options = [
 *     { mime: 'text/plain', original: false }, --> ficher txt généré par LoadIstex (choix n°1)
 *     { mime: 'text/plain'}                    --> ficher txt (choix n°2, seulement s'il n'y a aucun choix n°1)
 *   ]
 * @param {array} files (jsonLine.metadata || jsonLine.fulltext)
 * @param {array} options Liste (ordonnées) des caractéristiques du document recherché
 * @return {object} L'objet correspondant le mieux aux critères ou null
 */
object.files.select = function(files, options) {
  for (var i = 0; i < options.length; i++) {
    var result = object.files.get(files, options[i]);
    if (result) return result;
  }
  return null;
};

/**
 * Retourne le premier objet du Tableau de fichier respectant tous les critères spécifiées
 * Exemple : Je souhaite récupérer le fichier txt généré par LoadIstex
 *   files = docObject.fulltext (paramètre du docObject contenant les infos liées au fulltext)
 *   criteria = { mime: 'text/plain', original: false }, --> ficher txt généré par LoadIstex
 * @param {array} files Tableau d'objet représentant un ensemble de fichier (ex : jsonLine.metadata || jsonLine.fulltext)
 * @param {object} criteria Objet regroupant les critères du document recherché
 * @return {object} L'objet correspondant ou null
 */
object.files.get = function(files, criteria) {
  var keys = Object.keys(criteria);
  for (var i = 0; i < files.length; i++) {
    var found = true;
    for (var j = 0; j < keys.length; j++) {
      found &= (criteria[keys[j]] instanceof RegExp) ? criteria[keys[j]].test(files[i][keys[j]]) : (files[i][keys[j]] === criteria[keys[j]]);
      if (!found) break;
    }
    if (found) return files[i];
  }
  return null;
};

/**
 * Retourne les infos nécessaires pour la lecture ou la création d'un fichier dans la chaîne Istex
 * Pour l'id: 0123456789012345678901234567890123456789
 *  - directory => [corpusPath]/0/1/2/0123456789012345678901234567890123456789/[type]/([label]/)
 *  - filename => 0123456789012345678901234567890123456789.([label].)[extension]
 * @param {object} options Objet comportant toutes les informations nécessaire à la création du chemin :
 *  - {string} corpusPath Chemin du corpusOutput
 *  - {string} id Id Istex du document
 *  - {string} type Type de document (metadata | enrichments | fulltext)
 *  - {string} label Label du module (ce qui permet d'ajouter un sous-répertoire dédié au module, utile dans le cas où plusieurs enrichissements différents peuvent être produits)
 *  - {string} extension Extension du document (ex : .tei.xml)
 * @return {object} fileInfos sous la forme : { filemane, directory }
 */
object.files.createPath = function(options) {
  var result = null;
  if (options && options.id) {
    result = {
      'directory': path.join(options.corpusPath, options.id[0], options.id[1], options.id[2], options.id, options.type, options.label),
      'filename': options.id + ((options.label) ? '.' : '') + options.label + options.extension
    };
  }
  return result;
};

// Regroupe les fonctions liées aux fichiers TEI dans la chaine LoadIstex
object.enrichments = {};

/**
 * Sauvegarde un enrichissement dans le jsonLine
 * @param {object} enrichments enrichments d'un jsonLine d'un docObject
 * @param {object} options Options :
 *   - {string} label Label du module
 *   - {object} enrichment Enrichissment à sauvegarder
 * @return {undefined} Return undefined
 */
object.enrichments.save = function(enrichments, options) {
  // Si jsonLine ne contient pas encore de clé enrichments
  if (!enrichments) enrichments = {};
  // Si enrichments[options.label] ne contient pas encore d'enrichissement
  if (!enrichments[options.label]) {
    enrichments[options.label] = [];
    enrichments[options.label].push(options.enrichment);
  } else {
    var isAlready = object.files.get(enrichments[options.label], options.enrichment);
    // Si l'objet n'est pas déjà dans le jsonLine
    if (!isAlready) {
      // Ajout de l'enrichissement
      enrichments[options.label].push(options.enrichment);
    }
  }
  return enrichments;
};

/**
 * Écrit un fichier de TEI
 * @param {object} options Objet comportant toutes les informations nécessaire à la création du chemin :
 *  - {string} template Chemin du Tempalte
 *  - {object} data Données à insérer dans le Template
 *  - {object} output Données sur l'Output (voir : object.files.createPath)
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreur de Lecture/Écriture
 * @return {undefined} Return undefined
 */
object.enrichments.write = function(options, cb) {
  // Récupération du fragment de TEI
  fs.readFile(options.template, 'utf-8', function(err, tpl) {
    // Lecture impossible
    if (err) return cb(err);
    // Si le répertoire n'existe pas
    mkdirp(options.output.directory, function(err, made) {
      // Erreur I/O
      if (err) return cb(err);
      // Construction du fragment depuis le template et du nom de fichier
      var fragment = mustache.render(tpl, options.data),
        filename = path.join(options.output.directory, options.output.filename);
      // Écriture du fragment de TEI
      fs.writeFile(filename, fragment, 'utf8', function(err) {
        return cb(err);
      });
    });
  });
};

// Regroupe les fonctions liées aux traitement des XML
object.XML = {};

/**
 * Parse le contenu d'un fichier XML
 * @param {string} xmlStr Sélecteur
 * @return {object} Objet JSON représentant le document XML ou null
 */
object.XML.load = function(xmlStr) {
  var result = cheerio.load(xmlStr, {
    xmlMode: true
  });
  return (Object.keys(result).length > 0) ? result : null;
};

// Regroupe les fonctions liées aux traitement des URL
object.URL = {};

/**
 * Construit l'url d'une requête http GET
 * @param {string} url Toutes la partie de l'url avant le '?'
 * @param {object} parameters Paramètres à ajouter à l'url (après le '?')
 * @return {string} L'url complète encodée
 */
object.URL.addParameters = function(url, parameters) {
  var keys = Object.keys(parameters),
    result = '?',
    separator = '&';
  for (var i = 0; i < keys.length; i++) {
    result += keys[i] + '=' + encodeURIComponent(parameters[keys[i]]) + ((i < keys.length - 1) ? '&' : '');
  }
  return url + result;
};

// Regroupe les fonctions liées aux dates
object.dates = {};

/**
 * Renvoi la date actuelle au format souhaité
 * @param {string} format Format de la date (par défaut 'dd-mm-yyyy')
 * @return {string} Date au format souhaité
 */
object.dates.now = function(format) {
  var arg = format || 'dd-mm-yyyy';
  return dateFormat(new Date(Date.now()), arg);
};

// Regroupe les fonctions liées aux services
object.services = {};

/**
 * Effectue une requête HTTP (méthode POST) sur un service
 * @param {object} options Objet comportant toutes les informations nécessaire à la requête :
 *  - {string} filename Nom du fichier
 *  - {object} headers Headers de la requête
 *  - {string} url Url d'accès au service 
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreur de Lecture/Écriture
 *  - {object} res Résultat de la réponse, sous la forme :
 *    - {object} httpResponse Réponse HTTP
 *    - {string} body Body de la réponse
 * @return {undefined} undefined
 */
object.services.post = function(options, cb) {
  var _err = null,
    _res = null;
  // Vérification de l'existence du fichier à envoyer
  fs.stat(options.filename, function(err, stats) {
    // Lecture impossible
    if (err) {
      _err = new Error(err);
      return cb(_err, _res);
    }
    // Création du form data
    var formData = {
      file: fs.createReadStream(options.filename)
    };
    // Requête POST sur le service
    request.post({
      headers: options.headers,
      url: options.url,
      formData: formData
    }, function(err, httpResponse, body) {
      // Erreur
      if (err) {
        _err = new Error(err);
        return cb(_err, _res);
      }
      // Retourne le résultat de la requête
      return cb(_err, {
        "httpResponse": httpResponse,
        "body": body
      });
    });
  });
};

/**
 * Applique une feuille xslt sur un document XML (provenant d'un service)
 * @param {object} options Objet comportant toutes les informations nécessaire à la création du chemin :
 *  - {string} output Chemin du Tempalte
 *  - {string} documentId Données à insérer dans le Template
 *  - {string} runId Données sur l'Output (voir : object.files.createPath)
 *  - {string} xsltFile Données sur l'Output (voir : object.files.createPath)
 *  - {string} xmlFile Données sur l'Output (voir : object.files.createPath)
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreur de Lecture/Écriture
 *  - {Object} res Résultat de l'opération, sous la forme 
 *    - {object} logs Logs de stderr (array) et stdout (array)
 *    - {array} output Tableau contenant les différents logs dans l'ordre d'apparition
 *    - {integer} code Code de retour du process
 * @return {undefined} Return undefined
 */
object.services.transformXML = function(options, cb) {
  // Spawn du process qui effectura la transformation XSLT
  var xsltproc = child_process.spawn('xsltproc', [
      '--output',
      options.output,
      '--stringparam',
      'documentId',
      options.documentId,
      '--stringparam',
      'runId',
      options.runId,
      options.xsltFile,
      options.xmlFile
    ]),
    _err = null,
    logs = {
      'stderr': [],
      'stdout': []
    },
    output = [];

  // Write stdout in Logs
  xsltproc.stdout.on('data', function(data) {
    var str = data.toString();
    logs.stdout.push(str);
    return output.push('[stdout] ' + str);
  });

  // Write stderr in Logs
  xsltproc.stderr.on('data', function(data) {
    var str = data.toString();
    logs.stderr.push(str);
    return output.push('[stderr] ' + data.toString());
  });

  // On error
  xsltproc.on('error', function(err) {
    var res = {
      'logs': logs,
      'output': output
    };
    return cb(err, res);
  });

  // On close
  xsltproc.on('close', function(code) {
    var res = {
      'logs': logs,
      'output': output,
      'code': code
    };
    return cb(null, res);
  });
};

module.exports = object;