<commit_before>/*!
 * Copyright 2013 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var events = require('events');
var fs = require('fs');
var less = require('less');
var path = require('path');
var readdirp = require('readdirp');
var util = require('util');
var watch = require('watch');

var ConfigAPI = require('oae-config');
var IO = require('oae-util/lib/io');
var Validator = require('oae-util/lib/validator').Validator;
var log = require('oae-logger').logger('oae-ui');

var UIConstants = require('./constants').UIConstants;

// The Config object for the UI module.
var uiConfig = ConfigAPI.config('oae-ui');

// The cached skin variables
var cachedSkinVariables = null;
// The cached skins per tenant.
var cachedSkins = {};


// Path to the 3akai-ux repository
var uiDirectory = null;

// A mapping object that maps pre-optimized paths to post-optimized paths in the UI
var hashes = null;

// A dictionary that will hold the content for each file. This will be lazy filled. The first
// time a particular file is requested, it will be cached. After that, the cached version will
// be used
var staticFileCache = {};

// A dictionary that will hold the widget manifests for all widgets. This will be filled upon
// initialization.
var widgetManifestCache = {};


/**
 * The UI API.
 *
 * ## Events
 *
 * * `skinParsed` - Invoked when the skin file has been parsed or re-parsed on the application node.
 */
var UIAPI = module.exports = new events.EventEmitter();
var emitter = UIAPI;

////////////////////
// Initialization //
////////////////////

/**
 * This will find all of the widget config files in the UI repository and cache. For development,
 * it will also put watches on the files in the UI repository in order to invalidate the cache
 * when a file has been changed/removed.
 *
 * @param  {String}     uiDirectory     The path to the directory containing the UI repository.
 * @param  {Object}     hashes          A mapping that will map pre-optimized paths to hashed, post-optimized static asset paths
 * @param  {Function}   callback        Standard callback method.
 * @param  {Object}     callback.err    An error object (if any)
 */
var init = module.exports.init = function(_uiDirectory, _hashes, callback) {
    // Cache the ui directory path and make sure we have the absolute path
    uiDirectory = _uiDirectory;
    hashes = _hashes;

    // Cache all of the widget manifest files
    cacheWidgetManifests();

    // Monitor the UI repository for changes and refresh the cache.
    // This will only be done in development mode
    if (process.env['NODE_ENV'] !== 'production') {
        watch.createMonitor(uiDirectory, {'ignoreDotFiles': true}, function(monitor) {
            monitor.on('created', updateFileCaches);
            monitor.on('changed', updateFileCaches);
            monitor.on('removed', updateFileCaches);
        });
    }

    // Cache the base skin file.
    _cacheSkinVariables(function(err) {
        if (err) {
            return callback(err);
        }

        // Ensure the skins are not cached, as they may be invalid now
        cachedSkins = {};
        return callback();
    });
};

ConfigAPI.on('update', function(tenantAlias) {
    // Re-generate the skin.
    // Don't delete it from the cache just yet as we might still be serving requests.
    _generateSkin(tenantAlias, function(err) {
        if (err) {
            log().error({'err': err, 'tenantAlias': tenantAlias}, 'Could not re-cache the tenant skin after a config update.');
        }

        emitter.emit('skinParsed');
    });
});


/////////////
// Caching //
/////////////

/**
 * When a file in the UI repository has changed, we update the static file cache by deleting
 * the record for that file if there is one. The next time the updated/created file will be
 * re-requested, caching will be attempted again. We also check if the changed file is a
 * widget manifest file, and refresh the widget manifest cache if that's the case
 *
 * @param  {String}     filename        The absolute path to the file that has been added/updated/deleted
 * @api private
 */
var updateFileCaches = function(filename) {
    filename = filename.replace(uiDirectory, '');
    // Delete the file from the static file cache, for it to be re-cached
    // when it is requested again
    delete staticFileCache[filename];
    // If the changed file is a widget config file, we re-cache the widget
    // config files
    if (/^\/node_modules\/(.*?)\/manifest.json$/.test(filename)) {
        cacheWidgetManifests();
    // If the changed file is the base skin file, we re-cache it.
    } else if (filename === UIConstants.paths.BASE_SKIN) {
        // The skin file has changed, reset the skin files, they will be lazy loaded.
        cachedSkins = {};

        // Retrieve the skin variables.
        _cacheSkinVariables(function(err) {
            if (err) {
                log().error({'err': err}, 'Could not cache the skin variables after a file update.');
            }
        });
    }
};

///////////////////////////////
// Widget config aggregation //
///////////////////////////////

/**
 * Get the aggregated list of widget manifests.
 *
 * @return {Object}     An object where each key is the widget id and the value is the widget manifest.
 */
var getWidgetManifests = module.exports.getWidgetManifests = function() {
    return widgetManifestCache;
};

/**
 * Cache all widget manifests under the UI's node_modules directory
 *
 * @api private
 */
var cacheWidgetManifests = function() {
    widgetManifestCache = {};
    // Cache all of the widget config files under node_modules
    readdirp({ 'root': uiDirectory + '/node_modules/', 'directoryFilter': ['!.bin'], 'fileFilter': 'manifest.json' })
        .on('data', function (entry) {
            // Extract the widget id from the path, which is expected
            // to be the final part of the path
            var widgetId = entry.parentDir.split(path.sep).pop();
            try {
                var manifestContent = fs.readFileSync(entry.fullPath, 'utf8');
                widgetManifestCache[widgetId] = JSON.parse(manifestContent);
            } catch (err) {
                widgetManifestCache[widgetId] = {};
                log().error({'err': err, 'widgetId': widgetId, 'path': entry.fullPath}, 'Could not parse the widget manifest file.');
            }
            widgetManifestCache[widgetId].id = widgetId;
            widgetManifestCache[widgetId].path = entry.parentDir + '/';
        })
        .on('warn', function (err) { log().warn({'err': err}, 'A non-fatal error occured whilst caching the config schema'); })
        .on('error', function (err) { log().error({'err': err}, 'A fatal error occured whilst caching the config schema'); });
};

//////////////////
// Static batch //
//////////////////
<commit_msg>Better variable naming<commit_after>/*!
 * Copyright 2013 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var events = require('events');
var fs = require('fs');
var less = require('less');
var path = require('path');
var readdirp = require('readdirp');
var util = require('util');
var watch = require('watch');

var ConfigAPI = require('oae-config');
var IO = require('oae-util/lib/io');
var Validator = require('oae-util/lib/validator').Validator;
var log = require('oae-logger').logger('oae-ui');

var UIConstants = require('./constants').UIConstants;

// The Config object for the UI module.
var uiConfig = ConfigAPI.config('oae-ui');

// The cached skin variables
var cachedSkinVariables = null;
// The cached skins per tenant.
var cachedSkins = {};


// Path to the 3akai-ux repository
var uiDirectory = null;

// A mapping object that maps pre-optimized paths to post-optimized paths in the UI
var hashes = null;

// A dictionary that will hold the content for each file. This will be lazy filled. The first
// time a particular file is requested, it will be cached. After that, the cached version will
// be used
var staticFileCache = {};

// A dictionary that will hold the widget manifests for all widgets. This will be filled upon
// initialization.
var widgetManifestCache = {};


/**
 * The UI API.
 *
 * ## Events
 *
 * * `skinParsed` - Invoked when the skin file has been parsed or re-parsed on the application node.
 */
var UIAPI = module.exports = new events.EventEmitter();
var emitter = UIAPI;

////////////////////
// Initialization //
////////////////////

/**
 * This will find all of the widget config files in the UI repository and cache. For development,
 * it will also put watches on the files in the UI repository in order to invalidate the cache
 * when a file has been changed/removed.
 *
 * @param  {String}     uiDirectory     The path to the directory containing the UI repository.
 * @param  {Object}     hashes          A mapping that will map pre-optimized paths to hashed, post-optimized static asset paths
 * @param  {Function}   callback        Standard callback method.
 * @param  {Object}     callback.err    An error object (if any)
 */
var init = module.exports.init = function(_uiDirectory, _hashes, callback) {
    // Cache the ui directory path and make sure we have the absolute path
    uiDirectory = _uiDirectory;
    hashes = _hashes;

    // Cache all of the widget manifest files
    cacheWidgetManifests();

    // Monitor the UI repository for changes and refresh the cache.
    // This will only be done in development mode
    if (process.env['NODE_ENV'] !== 'production') {
        watch.createMonitor(uiDirectory, {'ignoreDotFiles': true}, function(monitor) {
            monitor.on('created', updateFileCaches);
            monitor.on('changed', updateFileCaches);
            monitor.on('removed', updateFileCaches);
        });
    }

    // Cache the base skin file.
    _cacheSkinVariables(function(err) {
        if (err) {
            return callback(err);
        }

        // Ensure the skins are not cached, as they may be invalid now
        cachedSkins = {};
        return callback();
    });
};

ConfigAPI.on('update', function(tenantAlias) {
    // Re-generate the skin.
    // Don't delete it from the cache just yet as we might still be serving requests.
    _generateSkin(tenantAlias, function(err) {
        if (err) {
            log().error({'err': err, 'tenantAlias': tenantAlias}, 'Could not re-cache the tenant skin after a config update.');
        }

        emitter.emit('skinParsed');
    });
});


/////////////
// Caching //
/////////////

/**
 * When a file in the UI repository has changed, we update the static file cache by deleting
 * the record for that file if there is one. The next time the updated/created file will be
 * re-requested, caching will be attempted again. We also check if the changed file is a
 * widget manifest file, and refresh the widget manifest cache if that's the case
 *
 * @param  {String}     filename        The absolute path to the file that has been added/updated/deleted
 * @api private
 */
var updateFileCaches = function(filename) {
    filename = filename.replace(uiDirectory, '');
    // Delete the file from the static file cache, for it to be re-cached
    // when it is requested again
    delete staticFileCache[filename];
    // If the changed file is a widget config file, we re-cache the widget
    // config files
    if (/^\/node_modules\/(.*?)\/manifest.json$/.test(filename)) {
        cacheWidgetManifests();
    // If the changed file is the base skin file, we re-cache it.
    } else if (filename === UIConstants.paths.BASE_SKIN) {
        // The skin file has changed, reset the skin files, they will be lazy loaded.
        cachedSkins = {};

        // Retrieve the skin variables.
        _cacheSkinVariables(function(err) {
            if (err) {
                log().error({'err': err}, 'Could not cache the skin variables after a file update.');
            }
        });
    }
};

///////////////////////////////
// Widget config aggregation //
///////////////////////////////

/**
 * Get the aggregated list of widget manifests.
 *
 * @return {Object}     An object where each key is the widget id and the value is the widget manifest.
 */
var getWidgetManifests = module.exports.getWidgetManifests = function() {
    return widgetManifestCache;
};

/**
 * Cache all widget manifests under the UI's node_modules directory
 *
 * @api private
 */
var cacheWidgetManifests = function() {
    widgetManifestCache = {};
    // Cache all of the widget config files under node_modules
    readdirp({ 'root': uiDirectory + '/node_modules/', 'directoryFilter': ['!.bin'], 'fileFilter': 'manifest.json' })
        .on('data', function (entry) {
            // Extract the widget id from the path, which is expected
            // to be the final part of the path
            var widgetId = entry.parentDir.split(path.sep).pop();
            try {
                var widgetManifest = fs.readFileSync(entry.fullPath, 'utf8');
                widgetManifestCache[widgetId] = JSON.parse(widgetManifest);
            } catch (err) {
                widgetManifestCache[widgetId] = {};
                log().error({'err': err, 'widgetId': widgetId, 'path': entry.fullPath}, 'Could not parse the widget manifest file.');
            }
            widgetManifestCache[widgetId].id = widgetId;
            widgetManifestCache[widgetId].path = entry.parentDir + '/';
        })
        .on('warn', function (err) { log().warn({'err': err}, 'A non-fatal error occured whilst caching the config schema'); })
        .on('error', function (err) { log().error({'err': err}, 'A fatal error occured whilst caching the config schema'); });
};

//////////////////
// Static batch //
//////////////////

{'fraction_non_alphanumeric': 0.12885797258053625, 'fraction_numerical': 0.0013507125008441953, 'mean_word_length': 2.6644394951744617, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3805324', 'n_tokens_mistral': 3926, 'n_tokens_neox': 3874, 'n_words': 1566}