Source: composer-common/lib/businessnetworkdefinition.js

/*
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-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.
 */

'use strict';

const Logger = require('./log/logger');
const ModelManager = require('./modelmanager');
const Introspector = require('./introspect/introspector');
const AclManager = require('./aclmanager');
const AclFile = require('./acl/aclfile');
const Factory = require('./factory');
const Serializer = require('./serializer');
const ScriptManager = require('./scriptmanager');
const JSZip = require('jszip');
const semver = require('semver');
const fs = require('fs');
const fsPath = require('path');
const minimatch = require('minimatch');

const ENCODING = 'utf8';
const LOG = Logger.getLog('BusinessNetworkDefinition');

/**
 * <p>
 * A BusinessNetworkDefinition defines a set of Participants that exchange Assets by
 * sending Transactions. This class manages the metadata and domain-specific types for
 * the network as well as a set of executable scripts.
 * </p>
 * @class
 * @memberof module:composer-common
 */
class BusinessNetworkDefinition {

    /**
     * Create the BusinessNetworkDefinition.
     * <p>
     * <strong>Note: Only to be called by framework code. Applications should
     * retrieve instances from {@link BusinessNetworkDefinition.fromArchive}</strong>
     * </p>
     * @param {String} identifier  - the identifier of the business network. The
     * identifier is formed from a business network name + '@' + version. The
     * version is a semver valid version string.
     * @param {String} description  - the description of the business network
     */
    constructor(identifier, description) {
        const method = 'constructor';
        LOG.entry(method, identifier, description);
        this.identifier = identifier;

        const atIndex = this.identifier.lastIndexOf('@');

        if (atIndex >= 0) {
            this.name = this.identifier.substring(0, atIndex);
        } else {
            throw new Error('Malformed business network identifier. It must be "name@major.minor.micro"');
        }

        this.version = this.identifier.substring(atIndex + 1);
        if (!semver.valid(this.version)) {
            throw new Error('Version number is invalid. Should be valid according to semver but found: ' + this.version);
        }

        this.description = description;
        this.modelManager = new ModelManager();
        this.aclManager = new AclManager(this.modelManager);
        this.scriptManager = new ScriptManager(this.modelManager);
        this.introspector = new Introspector(this.modelManager);
        this.factory = new Factory(this.modelManager);
        this.serializer = new Serializer(this.factory, this.modelManager);
        LOG.exit(method);
    }

    /**
     * Returns the identifier for this business network
     * @return {String} the identifier of this business network
     */
    getIdentifier() {
        return this.identifier;
    }

    /**
     * Returns the name for this business network
     * @return {String} the name of this business network
     */
    getName() {
        return this.name;
    }

    /**
     * Returns the version for this business network
     * @return {String} the version of this business network. Use semver module
     * to parse.
     */
    getVersion() {
        return this.version;
    }


    /**
     * Returns the description for this business network
     * @return {String} the description of this business network
     */
    getDescription() {
        return this.description;
    }

    /**
     * Create a BusinessNetworkDefinition from an archive.
     * @param {Buffer} Buffer  - the Buffer to a zip archive
     * @return {Promise} a Promise to the instantiated business network
     */
    static fromArchive(Buffer) {
        const method = 'fromArchive';
        LOG.entry(method, Buffer.length);
        return JSZip.loadAsync(Buffer).then(function(zip) {
            const allPromises = [];
            let ctoModelFiles = [];
            let jsScriptFiles = [];
            let permissionsFiles = [];
            let businessNetworkDefinition;

            LOG.debug(method, 'Loading package.json');
            let packageJson = zip.file('package.json');
            if (packageJson === null) {
                throw Error('package.json must exist');
            }
            const packagePromise = packageJson.async('string');
            allPromises.push(packagePromise);
            packagePromise.then(contents => {
                LOG.debug(method, 'Loaded package.json');
                let jsonObject = JSON.parse(contents);
                let packageName = jsonObject.name;
                let packageVersion = jsonObject.version;
                let packageDescription = jsonObject.description;
                businessNetworkDefinition = new BusinessNetworkDefinition(packageName + '@' + packageVersion, packageDescription);
            });

            LOG.debug(method, 'Looking for model files');
            let ctoFiles = zip.file(/models\/.*\.cto$/); //Matches any file which is in the 'models' folder and has a .cto extension
            ctoFiles.forEach(function(file) {
                LOG.debug(method, 'Found model file, loading it', file.name);
                const ctoPromise = file.async('string');
                allPromises.push(ctoPromise);
                ctoPromise.then(contents => {
                    LOG.debug(method, 'Loaded model file');
                    ctoModelFiles.push(contents);
                });
            });

            LOG.debug(method, 'Looking for JavaScript files');
            let jsFiles = zip.file(/lib\/.*\.js$/); //Matches any file which is in the 'lib' folder and has a .js extension
            jsFiles.forEach(function(file) {
                LOG.debug(method, 'Found JavaScript file, loading it', file.name);
                const jsPromise = file.async('string');
                allPromises.push(jsPromise);
                jsPromise.then(contents => {
                    LOG.debug(method, 'Loaded JavaScript file');
                    let tempObj = {
                        'name': file.name,
                        'contents': contents
                    };
                    jsScriptFiles.push(tempObj);

                });
            });

            LOG.debug(method, 'Loading permissions.acl');
            let aclFile = zip.file('permissions.acl');
            if (aclFile !== null) {
                const aclPromise = aclFile.async('string');
                allPromises.push(aclPromise);
                aclPromise.then(contents => {
                    LOG.debug(method, 'Loaded permissions.acl');
                    permissionsFiles.push(contents);
                });
            }

            return Promise.all(allPromises)
                .then(() => {
                    LOG.debug(method, 'Loaded all model, JavaScript, and ACL files');
                    LOG.debug(method, 'Adding model files to model manager');
                    businessNetworkDefinition.modelManager.addModelFiles(ctoModelFiles); // Adds all cto files to model manager
                    LOG.debug(method, 'Added model files to model manager');
                    // console.log('What are the jsObjectsArray?',jsObjectArray);
                    LOG.debug(method, 'Adding JavaScript files to script manager');
                    jsScriptFiles.forEach(function(obj) {
                        let jsObject = businessNetworkDefinition.scriptManager.createScript(obj.name, 'js', obj.contents);
                        businessNetworkDefinition.scriptManager.addScript(jsObject); // Adds all js files to script manager
                    });
                    LOG.debug(method, 'Added JavaScript files to script manager');
                    LOG.debug(method, 'Adding ACL files to ACL manager');
                    permissionsFiles.forEach((permissionFile) => {
                        businessNetworkDefinition.getAclManager().setAclFile( new AclFile('permissions.acl', businessNetworkDefinition.getModelManager(), permissionFile));
                    });
                    LOG.debug(method, 'Added ACL files to ACL manager');

                    LOG.exit(method, businessNetworkDefinition.toString());
                    return businessNetworkDefinition; // Returns business network (with model manager and script manager)
                });
        });
    }

    /**
     * Store a BusinessNetworkDefinition as an archive.
     * @return {Buffer} buffer  - the zlib buffer
     */
    toArchive() {

        let zip = new JSZip();

        let packageFileContents = JSON.stringify({
            name: this.name,
            version: this.version,
            description: this.description
        });

        zip.file('package.json', packageFileContents);

        const aclFile = this.getAclManager().getAclFile();
        if(aclFile) {
            zip.file(aclFile.getIdentifier(), aclFile.definitions);
        }

        let modelManager = this.getModelManager();
        let modelFiles = modelManager.getModelFiles();
        modelFiles.forEach(function(file) {
            zip.folder('models').file(file.namespace + '.cto', file.definitions);
        });

        let scriptManager = this.getScriptManager();
        let scriptFiles = scriptManager.getScripts();
        scriptFiles.forEach(function(file) {
            let fileIdentifier = file.identifier;
            let fileName = fileIdentifier.substring(fileIdentifier.lastIndexOf('/') + 1);
            zip.folder('lib').file(fileName, file.contents);
        });

        return zip.generateAsync({
            type: 'nodebuffer'
        }).then(something => {
            return Promise.resolve(something).then(result => {
                return result;
            });

        });

    }

    /**
     * Builds a BusinessNetworkDefintion from the contents of a directory.
     * The directory must include a package.json in the root (used to specify
     * the name, version and description of the business network). This method
     * is designed to work with business networks that refer to external models
     * using npm dependencies as well as business networks that statically
     * package their model files.
     * <p>
     * If package.json contains a dependencies property then this method will search for
     * model (CTO) files under the node_modules directory for each dependency that
     * passes the options.dependencyGlob pattern.
     * </p>
     * <p>
     * In addition all model files will be added that are not under node_modules
     * and that pass the options.modelFileGlob pattern. By default you should put
     * model files under a directory called 'models'.
     * </p>
     * <p>
     * All script (js) files will be added that are not under node_modules and
     * that pass the options.scriptGlob pattern. By default you should put Javascript
     * files under the 'lib' directory.
     * </p>
     *
     * @param {String} path to a local directory
     * @param {Object} [options] - an optional set of options to configure the instance.
     * @param {Object} [options.dependencyGlob] - specify the glob pattern used to match
     * the npm dependencies to process. Defaults to **
     * @param {boolean} [options.modelFileGlob] - specify the glob pattern used to match
     * the model files to include. Defaults to **\/models/**\/*.cto
     * @param {boolean} [options.scriptGlob] - specify the glob pattern used to match
     * the script files to include. Defaults to **\/lib/**\/*.js
     * @return {Promise} a Promise to the instantiated business network
     */
    static fromDirectory(path, options) {

        if(!options) {
            options = {};
        }

        if(!options.dependencyGlob) {
            options.dependencyGlob = '**';
        }

        if(!options.modelFileGlob) {
            options.modelFileGlob = '**/models/**/*.cto';
        }

        if(!options.scriptGlob) {
            options.scriptGlob = '**/lib/**/*.js';
        }

        const method = 'fromDirectory';
        LOG.entry(method, path);

        // grab the package.json
        let packageJsonContents = fs.readFileSync( fsPath.resolve(path, 'package.json'), ENCODING);

        if(!packageJsonContents) {
            throw new Error('Failed to find package.json');
        }

        LOG.debug(method, 'Loaded package.json', packageJsonContents);

        // parse the package.json
        let jsonObject = JSON.parse(packageJsonContents);
        let packageName = jsonObject.name;
        let packageVersion = jsonObject.version;
        let packageDescription = jsonObject.description;

        // create the business network definition
        const businessNetwork = new BusinessNetworkDefinition(packageName + '@' + packageVersion, packageDescription);
        const modelFiles = [];

        // process each module dependency
        // filtering using a glob on the module dependency name
        if(jsonObject.dependencies) {
            LOG.debug(method, 'All dependencies', Object.keys(jsonObject.dependencies).toString());
            const dependencies = Object.keys(jsonObject.dependencies).filter(minimatch.filter(options.dependencyGlob));
            LOG.debug(method, 'Matched dependencies', dependencies);

            for( let dep of dependencies) {
                // find all the *.cto files under the npm install dependency path
                let dependencyPath = fsPath.resolve(path, 'node_modules', dep);
                LOG.debug(method, 'Checking dependency path', dependencyPath);
                if (!fs.existsSync(dependencyPath)) {
                    // need to check to see if this is in a peer directory as well
                    //
                    LOG.debug(method,'trying different path '+path.replace(packageName,''));
                    dependencyPath = fsPath.resolve(path.replace(packageName,''),dep);
                    if(!fs.existsSync(dependencyPath)){
                        throw new Error('npm dependency path ' + dependencyPath + ' does not exist. Did you run npm install?');
                    }
                }

                BusinessNetworkDefinition.processDirectory(dependencyPath, {
                    accepts: function(file) {
                        return minimatch(file, options.modelFileGlob);
                    },
                    acceptsDir: function(dir) {
                        return true;
                    },
                    process: function(path,contents) {
                        modelFiles.push(contents);
                        LOG.debug(method, 'Found model file', path);
                    }
                });
            }
        }

        // define a help function that will filter out files
        // that are inside a node_modules directory under the path
        // we are processing
        const isFileInNodeModuleDir = function(file) {
            const method = 'isFileInNodeModuleDir';
            let filePath = fsPath.parse(file);
            let subPath = filePath.dir.substring(path.length);
            let result = subPath.split(fsPath.sep).some((element) => {
                return element === 'node_modules';
            });

            LOG.debug(method, file, result);
            return result;
        };

        // find CTO files outside the npm install directory
        //
        BusinessNetworkDefinition.processDirectory(path, {
            accepts: function(file) {
                return isFileInNodeModuleDir(file) === false && minimatch(file, options.modelFileGlob);
            },
            acceptsDir: function(dir) {
                return !isFileInNodeModuleDir(dir);
            },
            process: function(path,contents) {
                modelFiles.push(contents);
                LOG.debug(method, 'Found model file', path);
            }
        });

        businessNetwork.getModelManager().addModelFiles(modelFiles);
        LOG.debug(method, 'Added model files',  modelFiles.length);

        // find script files outside the npm install directory
        const scriptFiles = [];
        BusinessNetworkDefinition.processDirectory(path, {
            accepts: function(file) {
                return isFileInNodeModuleDir(file) === false && minimatch(file, options.scriptGlob);
            },
            acceptsDir: function(dir) {
                return !isFileInNodeModuleDir(dir);
            },
            process: function(path,contents) {
                let filePath = fsPath.parse(path);
                const jsScript = businessNetwork.getScriptManager().createScript(path, filePath.ext.toLowerCase(), contents);
                scriptFiles.push(jsScript);
                LOG.debug(method, 'Found script file ', path);
            }
        });

        if(modelFiles.length === 0) {
            throw new Error('Failed to find a model file.');
        }

        for( let script of scriptFiles) {
            businessNetwork.getScriptManager().addScript(script);
        }

        LOG.debug(method, 'Added script files', scriptFiles.length);

        // grab the permissions.acl
        const aclPath = fsPath.resolve(path, 'permissions.acl');
        if(fs.existsSync(aclPath)) {
            let permissionsAclContents = fs.readFileSync( aclPath, ENCODING);

            if(permissionsAclContents) {
                LOG.debug(method, 'Loaded permissions.acl', permissionsAclContents);
                const aclFile = new AclFile('permissions.acl', businessNetwork.getModelManager(), permissionsAclContents);
                businessNetwork.getAclManager().setAclFile(aclFile);
            }
        }

        LOG.exit(method, path);
        return Promise.resolve(businessNetwork);
    }

    /**
     * @param {String} path - the path to process
     * @param {Object} fileProcessor - the file processor. It must have
     * accept and process methods.
     * @private
     */
    static processDirectory(path, fileProcessor) {
        const items = BusinessNetworkDefinition.walkSync(path, [], fileProcessor);
        items.sort();
        LOG.debug('processDirectory', 'Path ' + path, items);
        items.forEach((item) => {
            BusinessNetworkDefinition.processFile(item, fileProcessor);
        });
    }

    /**
     * @param {String} file - the file to process
     * @param {Object} fileProcessor - the file processor. It must have
     * accepts and process methods.
     * @private
     */
    static processFile(file, fileProcessor) {

        if (fileProcessor.accepts(file)) {
            LOG.debug('processFile', 'FileProcessor accepted', file );
            let fileContents = fs.readFileSync(file, ENCODING);
            fileProcessor.process(file, fileContents);
        }
        else {
            LOG.debug('processFile', 'FileProcessor rejected', file );
        }
    }


    /**
     * @param {String} dir - the dir to walk
     * @param {Object[]} filelist - input files
     * @param {Object} fileProcessor - the file processor. It must have
     * accepts and process methods.
     * @return {Object[]} filelist - output files
     * @private
     */
    static walkSync(dir, filelist, fileProcessor) {
        let files = fs.readdirSync(dir);
        files.forEach(function (file) {
            let nestedPath = fsPath.join(dir, file);
            if (fs.lstatSync(nestedPath).isDirectory()) {
                if (fileProcessor.acceptsDir(nestedPath)) {
                    filelist = BusinessNetworkDefinition.walkSync(nestedPath, filelist, fileProcessor);
                }
            } else {
                filelist.push(nestedPath);
            }
        });
        return filelist;
    }


    /**
     * Visitor design pattern
     * @param {Object} visitor - the visitor
     * @param {Object} parameters  - the parameter
     * @return {Object} the result of visiting or null
     * @private
     */
    accept(visitor, parameters) {
        return visitor.visit(this, parameters);
    }

    /**
     * Provides access to the Introspector for this business network. The Introspector
     * is used to reflect on the types defined within this business network.
     * @return {Introspector} the Introspector for this business network
     */
    getIntrospector() {
        return this.introspector;
    }

    /**
     * Provides access to the Factory for this business network. The Factory
     * is used to create the types defined in this business network.
     * @return {Factory} the Factory for this business network
     */
    getFactory() {
        return this.factory;
    }

    /**
     * Provides access to the Serializer for this business network. The Serializer
     * is used to serialize instances of the types defined within this business network.
     * @return {Serializer} the Serializer for this business network
     */
    getSerializer() {
        return this.serializer;
    }

    /**
     * Provides access to the ScriptManager for this business network. The ScriptManager
     * manage access to the scripts that have been defined within this business network.
     * @return {ScriptManager} the ScriptManager for this business network
     * @private
     */
    getScriptManager() {
        return this.scriptManager;
    }

    /**
     * Provides access to the AclManager for this business network. The AclManager
     * manage access to the access conrol rules that have been defined for this business network.
     * @return {AclManager} the AclManager for this business network
     * @private
     */
    getAclManager() {
        return this.aclManager;
    }

    /**
     * Provides access to the ModelManager for this business network. The ModelManager
     * manage access to the models that have been defined within this business network.
     * @return {ModelManager} the ModelManager for this business network
     * @private
     */
    getModelManager() {
        return this.modelManager;
    }
}

module.exports = BusinessNetworkDefinition;