/*
* 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;