UNPKG

28.1 kBJavaScriptView Raw
1/*
2 * Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14
15'use strict';
16
17const fs = require('fs');
18const fsPath = require('path');
19const slash = require('slash');
20
21const DefaultModelFileLoader = require('./introspect/loaders/defaultmodelfileloader');
22const Factory = require('./factory');
23const Globalize = require('./globalize');
24const IllegalModelException = require('./introspect/illegalmodelexception');
25const ModelFile = require('./introspect/modelfile');
26const ModelFileDownloader = require('./introspect/loaders/modelfiledownloader');
27const ModelUtil = require('./modelutil');
28const Serializer = require('./serializer');
29const TypeNotFoundException = require('./typenotfoundexception');
30
31const debug = require('debug')('concerto:ModelManager');
32
33/**
34 * Manages the Concerto model files.
35 *
36 * The structure of {@link Resource}s (Assets, Transactions, Participants) is modelled
37 * in a set of Concerto files. The contents of these files are managed
38 * by the {@link ModelManager}. Each Concerto file has a single namespace and contains
39 * a set of asset, transaction and participant type definitions.
40 *
41 * Concerto applications load their Concerto files and then call the {@link ModelManager#addModelFile addModelFile}
42 * method to register the Concerto file(s) with the ModelManager. The ModelManager
43 * parses the text of the Concerto file and will make all defined types available
44 * to other Concerto services, such as the {@link Serializer} (to convert instances to/from JSON)
45 * and {@link Factory} (to create instances).
46 *
47 * @class
48 * @memberof module:concerto-core
49 */
50class ModelManager {
51 /**
52 * Create the ModelManager.
53 */
54 constructor() {
55 this.modelFiles = {};
56 this.factory = new Factory(this);
57 this.serializer = new Serializer(this.factory, this);
58 this.decoratorFactories = [];
59 this.systemModelTable = new Map();
60 this._isModelManager = true;
61 }
62
63 /**
64 * Visitor design pattern
65 * @param {Object} visitor - the visitor
66 * @param {Object} parameters - the parameter
67 * @return {Object} the result of visiting or null
68 * @private
69 */
70 accept(visitor, parameters) {
71 return visitor.visit(this, parameters);
72 }
73
74 /**
75 * Validates a Concerto file (as a string) to the ModelManager.
76 * Concerto files have a single namespace.
77 *
78 * Note that if there are dependencies between multiple files the files
79 * must be added in dependency order, or the addModelFiles method can be
80 * used to add a set of files irrespective of dependencies.
81 * @param {string} modelFile - The Concerto file as a string
82 * @param {string} fileName - an optional file name to associate with the model file
83 * @throws {IllegalModelException}
84 */
85 validateModelFile(modelFile, fileName) {
86 if (typeof modelFile === 'string') {
87 let m = new ModelFile(this, modelFile, fileName);
88 m.validate();
89 } else {
90 modelFile.validate();
91 }
92 }
93
94 /**
95 * Throws an error with details about the existing namespace.
96 * @param {ModelFile} modelFile The model file that is trying to declare an existing namespace
97 * @private
98 */
99 _throwAlreadyExists(modelFile) {
100 const existingModelFileName = this.modelFiles[modelFile.getNamespace()].getName();
101 const postfix = existingModelFileName ? ` in file ${existingModelFileName}` : '';
102 const prefix = modelFile.getName() ? ` specified in file ${modelFile.getName()}` : '';
103 let errMsg = `Namespace ${modelFile.getNamespace()}${prefix} is already declared${postfix}`;
104 throw new Error(errMsg);
105 }
106
107 /**
108 * Adds a Concerto file (as a string) to the ModelManager.
109 * Concerto files have a single namespace. If a Concerto file with the
110 * same namespace has already been added to the ModelManager then it
111 * will be replaced.
112 * Note that if there are dependencies between multiple files the files
113 * must be added in dependency order, or the addModelFiles method can be
114 * used to add a set of files irrespective of dependencies.
115 * @param {string} modelFile - The Concerto file as a string
116 * @param {string} fileName - an optional file name to associate with the model file
117 * @param {boolean} [disableValidation] - If true then the model files are not validated
118 * @param {boolean} [systemModelTable] - A table that maps classes in the new models to system types
119 * @throws {IllegalModelException}
120 * @return {Object} The newly added model file (internal).
121 */
122 addModelFile(modelFile, fileName, disableValidation,systemModelTable) {
123 const NAME = 'addModelFile';
124 debug(NAME, 'addModelFile', modelFile, fileName);
125
126 let m = null;
127
128 // Update the system model table with either the provided table or the default one
129 const isSystemModelFile = typeof systemModelTable !== 'undefined';
130 if(isSystemModelFile) {
131 if(typeof systemModelTable === 'object') {
132 systemModelTable.forEach((key, value) => this.systemModelTable.set(key, value));
133 } else {
134 ModelUtil.getIdentitySystemModelTable().forEach((key, value) => this.systemModelTable.set(key, value));
135 }
136 }
137
138 if (typeof modelFile === 'string') {
139 m = new ModelFile(this, modelFile, fileName, systemModelTable !== undefined);
140 } else {
141 m = modelFile;
142 }
143
144 if (!this.modelFiles[m.getNamespace()]) {
145 if (!disableValidation) {
146 m.validate();
147 }
148 this.modelFiles[m.getNamespace()] = m;
149 } else {
150 this._throwAlreadyExists(m);
151 }
152
153 return m;
154 }
155
156 /**
157 * @return {Map} A table that maps classes in the new models to system types
158 * @private
159 */
160 getSystemModelTable() {
161 return this.systemModelTable;
162 }
163
164 /**
165 * Updates a Concerto file (as a string) on the ModelManager.
166 * Concerto files have a single namespace. If a Concerto file with the
167 * same namespace has already been added to the ModelManager then it
168 * will be replaced.
169 * @param {string} modelFile - The Concerto file as a string
170 * @param {string} fileName - an optional file name to associate with the model file
171 * @param {boolean} [disableValidation] - If true then the model files are not validated
172 * @throws {IllegalModelException}
173 * @returns {Object} The newly added model file (internal).
174 */
175 updateModelFile(modelFile, fileName, disableValidation) {
176 const NAME = 'updateModelFile';
177 debug(NAME, 'updateModelFile', modelFile, fileName);
178 if (typeof modelFile === 'string') {
179 let m = new ModelFile(this, modelFile, fileName);
180 return this.updateModelFile(m,fileName,disableValidation);
181 } else {
182 let existing = this.modelFiles[modelFile.getNamespace()];
183 if (!existing) {
184 throw new Error('model file does not exist');
185 } else if (existing.isSystemModelFile()) {
186 throw new Error('System namespace can not be updated');
187 }
188 if (!disableValidation) {
189 modelFile.validate();
190 }
191 this.modelFiles[modelFile.getNamespace()] = modelFile;
192 return modelFile;
193 }
194 }
195
196 /**
197 * Remove the Concerto file for a given namespace
198 * @param {string} namespace - The namespace of the model file to
199 * delete.
200 */
201 deleteModelFile(namespace) {
202 if (!this.modelFiles[namespace]) {
203 throw new Error('model file does not exist');
204 } else if (this.modelFiles[namespace].isSystemModelFile()) {
205 throw new Error('Cannot delete system namespace');
206 } else {
207 delete this.modelFiles[namespace];
208 }
209 }
210
211 /**
212 * Add a set of Concerto files to the model manager.
213 * @param {string[]} modelFiles - An array of Concerto files as
214 * strings.
215 * @param {string[]} [fileNames] - An optional array of file names to
216 * associate with the model files
217 * @param {boolean} [disableValidation] - If true then the model files are not validated
218 * @param {boolean} [systemModelTable] - A table that maps classes in the new models to system types
219 * @returns {Object[]} The newly added model files (internal).
220 */
221 addModelFiles(modelFiles, fileNames, disableValidation,systemModelTable) {
222 const NAME = 'addModelFiles';
223 debug(NAME, 'addModelFiles', modelFiles, fileNames);
224 const originalModelFiles = {};
225 const originalSystemModelTable = {};
226 Object.assign(originalModelFiles, this.modelFiles);
227 Object.assign(originalSystemModelTable, this.systemModelTable);
228 let newModelFiles = [];
229
230 try {
231 // Update the system model table with either the provided table or the default one
232 const isSystemModelFile = typeof systemModelTable !== 'undefined';
233 if(isSystemModelFile) {
234 if(typeof systemModelTable === 'object') {
235 systemModelTable.forEach((key, value) => this.systemModelTable.set(key, value));
236 } else {
237 ModelUtil.getIdentitySystemModelTable().forEach((key, value) => this.systemModelTable.set(key, value));
238 }
239 }
240
241 // create the model files
242 for (let n = 0; n < modelFiles.length; n++) {
243 const modelFile = modelFiles[n];
244 let fileName = null;
245
246 if (fileNames) {
247 fileName = fileNames[n];
248 }
249
250 if (typeof modelFile === 'string') {
251 let m = new ModelFile(this, modelFile, fileName, isSystemModelFile);
252 if (m.isSystemModelFile()) {
253 throw new Error('System namespace can not be updated');
254 }
255 if (!this.modelFiles[m.getNamespace()]) {
256 this.modelFiles[m.getNamespace()] = m;
257 newModelFiles.push(m);
258 } else {
259 this._throwAlreadyExists(m);
260 }
261 } else {
262 if (modelFile.isSystemModelFile()) {
263 throw new Error('System namespace can not be updated');
264 }
265 if (!this.modelFiles[modelFile.getNamespace()]) {
266 this.modelFiles[modelFile.getNamespace()] = modelFile;
267 newModelFiles.push(modelFile);
268 } else {
269 this._throwAlreadyExists(modelFile);
270 }
271 }
272 }
273
274 // re-validate all the model files
275 if (!disableValidation) {
276 this.validateModelFiles();
277 }
278
279 // return the model files.
280 return newModelFiles;
281 } catch (err) {
282 this.modelFiles = {};
283 this.systemModelTable = {};
284 Object.assign(this.modelFiles, originalModelFiles);
285 Object.assign(this.systemModelTable, originalSystemModelTable);
286 throw err;
287 } finally {
288 debug(NAME, newModelFiles);
289 }
290 }
291
292
293 /**
294 * Validates all models files in this model manager
295 */
296 validateModelFiles() {
297 for (let ns in this.modelFiles) {
298 this.modelFiles[ns].validate();
299 }
300 }
301
302 /**
303 * Downloads all ModelFiles that are external dependencies and adds or
304 * updates them in this ModelManager.
305 * @param {Object} [options] - Options object passed to ModelFileLoaders
306 * @param {ModelFileDownloader} [modelFileDownloader] - an optional ModelFileDownloader
307 * @throws {IllegalModelException} if the models fail validation
308 * @return {Promise} a promise when the download and update operation is completed.
309 */
310 async updateExternalModels(options, modelFileDownloader) {
311
312 const NAME = 'updateExternalModels';
313 debug(NAME, 'updateExternalModels', options);
314
315 if(!modelFileDownloader) {
316 modelFileDownloader = new ModelFileDownloader(new DefaultModelFileLoader(this));
317 }
318
319 const externalModelFiles = await modelFileDownloader.downloadExternalDependencies(this.getModelFiles(), options)
320 .catch(error => {
321 // If we're not able to download the latest dependencies, see whether the models all validate based on the available cached models.
322 if(error.code === 'MISSING_DEPENDENCY'){
323 try {
324 this.validateModelFiles();
325 return [];
326 } catch (validationError){
327 // The validation error tells us the first model that is missing from the model manager, but the dependency download
328 // will fail at the first external model, regardless of whether there is already a local copy.
329 // As a hint to the user we display the URL of the external model that can't be found.
330 const modelFile = this.getModelFileByFileName(validationError.fileName);
331 const namespaces = modelFile.getExternalImports();
332 const missingNs = Object.keys(namespaces).find((ns) => validationError.shortMessage.includes(ns));
333 const url = modelFile.getImportURI(missingNs);
334 const err = new Error(`Unable to download external model dependency '${url}'`);
335 err.code = 'MISSING_DEPENDENCY';
336 throw err;
337 }
338 } else {
339 throw error;
340 }
341 });
342 const originalModelFiles = {};
343 Object.assign(originalModelFiles, this.modelFiles);
344
345 try {
346 externalModelFiles.forEach((mf) => {
347 const existing = this.modelFiles[mf.getNamespace()];
348
349 if (existing) {
350 this.updateModelFile(mf, mf.getName(), true); // disable validation
351 } else {
352 this.addModelFile(mf, mf.getName(), true); // disable validation
353 }
354 });
355
356 // now everything is applied, we need to revalidate all models
357 this.validateModelFiles();
358 return externalModelFiles;
359 } catch (err) {
360 this.modelFiles = {};
361 Object.assign(this.modelFiles, originalModelFiles);
362 throw err;
363 }
364 }
365
366 /**
367 * Write all models in this model manager to the specified path in the file system
368 *
369 * @param {String} path to a local directory
370 * @param {Object} [options] - Options object
371 * @param {boolean} options.includeExternalModels -
372 * If true, external models are written to the file system. Defaults to true
373 * @param {boolean} options.includeSystemModels -
374 * If true, system models are written to the file system. Defaults to false
375 */
376 writeModelsToFileSystem(path, options = {}) {
377 if(!path){
378 throw new Error('`path` is a required parameter of writeModelsToFileSystem');
379 }
380
381 const opts = Object.assign({
382 includeExternalModels: true,
383 includeSystemModels: false,
384 }, options);
385
386 this.getModelFiles().forEach(function (file) {
387 if (file.isSystemModelFile() && !opts.includeSystemModels) {
388 return;
389 }
390 if (file.isExternal() && !opts.includeExternalModels) {
391 return;
392 }
393 // Always assume file names have been normalized from `\` to `/`
394 const filename = slash(file.fileName).split('/').pop();
395 fs.writeFileSync(path + fsPath.sep + filename, file.definitions);
396 });
397 }
398
399 /**
400 * Get the array of model file instances
401 * Note - this is an internal method and therefore will return the system model
402 * as well as any network defined models.
403 *
404 * It is the callers responsibility to remove this before the data leaves an external API
405 *
406 * @return {ModelFile[]} The ModelFiles registered
407 * @private
408 */
409 getModelFiles() {
410 let keys = Object.keys(this.modelFiles);
411 let result = [];
412
413 for (let n = 0; n < keys.length; n++) {
414 result.push(this.modelFiles[keys[n]]);
415 }
416
417 return result;
418 }
419
420 /**
421 * Get the array of system model file instances
422 * Note - this is an internal method and therefore will return the system model
423 *
424 * @return {ModelFile[]} The system ModelFiles registered
425 * @private
426 */
427 getSystemModelFiles() {
428 return this.getModelFiles()
429 .filter((modelFile) => {
430 return modelFile.isSystemModelFile();
431 });
432 }
433
434 /**
435 * Gets all the CTO models
436 * @param {Object} [options] - Options object
437 * @param {boolean} options.includeExternalModels -
438 * If true, external models are written to the file system. Defaults to true
439 * @param {boolean} options.includeSystemModels -
440 * If true, system models are written to the file system. Defaults to false
441 * @return {Array<{name:string, content:string}>} the name and content of each CTO file
442 */
443 getModels(options) {
444 const modelFiles = this.getModelFiles();
445 let models = [];
446 const opts = Object.assign({
447 includeExternalModels: true,
448 includeSystemModels: false,
449 }, options);
450
451 modelFiles.forEach(function (file) {
452 if (file.isSystemModelFile() && !opts.includeSystemModels) {
453 return;
454 }
455 if (file.isExternal() && !opts.includeExternalModels) {
456 return;
457 }
458 let fileName;
459 if (file.fileName === 'UNKNOWN' || file.fileName === null || !file.fileName) {
460 fileName = file.namespace + '.cto';
461 } else {
462 let fileIdentifier = file.fileName;
463 fileName = fsPath.basename(fileIdentifier);
464 }
465 models.push({ 'name' : fileName, 'content' : file.definitions });
466 });
467 return models;
468 }
469
470 /**
471 * Check that the type is valid and returns the FQN of the type.
472 * @param {string} context - error reporting context
473 * @param {string} type - fully qualified type name
474 * @return {string} - the resolved type name (fully qualified)
475 * @throws {IllegalModelException} - if the type is not defined
476 * @private
477 */
478 resolveType(context, type) {
479 // is the type a primitive?
480 if (ModelUtil.isPrimitiveType(type)) {
481 return type;
482 }
483
484 let ns = ModelUtil.getNamespace(type);
485 let modelFile = this.getModelFile(ns);
486 if (!modelFile) {
487 let formatter = Globalize.messageFormatter('modelmanager-resolvetype-nonsfortype');
488 throw new IllegalModelException(formatter({
489 type: type,
490 context: context
491 }));
492 }
493
494 if (modelFile.isLocalType(type)) {
495 return type;
496 }
497
498 let formatter = Globalize.messageFormatter('modelmanager-resolvetype-notypeinnsforcontext');
499 throw new IllegalModelException(formatter({
500 context: context,
501 type: type,
502 namespace: modelFile.getNamespace()
503 }));
504 }
505
506 /**
507 * Remove all registered Concerto files
508 */
509 clearModelFiles() {
510 const systemModelFiles = this.getSystemModelFiles();
511 this.modelFiles = {};
512
513 systemModelFiles.forEach((m) => {
514 this.modelFiles[m.getNamespace()] = m;
515 });
516
517 // now validate all the models
518 this.validateModelFiles();
519 }
520
521 /**
522 * Get the ModelFile associated with a namespace
523 * Note - this is an internal method and therefore will return the system model
524 * as well as any network defined models.
525 *
526 * It is the callers responsibility to remove this before the data leaves an external API
527 * @param {string} namespace - the namespace containing the ModelFile
528 * @return {ModelFile} registered ModelFile for the namespace or null
529 * @private
530 */
531 getModelFile(namespace) {
532 return this.modelFiles[namespace];
533 }
534
535 /**
536 * Get the ModelFile associated with a file name
537 * Note - this is an internal method and therefore will return the system model
538 * as well as any network defined models.
539 *
540 * It is the callers responsibility to remove this before the data leaves an external API
541 * @param {string} fileName - the fileName associated with the ModelFile
542 * @return {ModelFile} registered ModelFile for the namespace or null
543 * @private
544 */
545 getModelFileByFileName(fileName) {
546 return this.getModelFiles().filter(mf => mf.getName() === fileName)[0];
547 }
548
549 /**
550 * Get the namespaces registered with the ModelManager.
551 * @return {string[]} namespaces - the namespaces that have been registered.
552 */
553 getNamespaces() {
554 return Object.keys(this.modelFiles);
555 }
556 /**
557 * Look up a type in all registered namespaces.
558 *
559 * @param {string} qualifiedName - fully qualified type name.
560 * @return {ClassDeclaration} - the class declaration for the specified type.
561 * @throws {TypeNotFoundException} - if the type cannot be found or is a primitive type.
562 * @private
563 */
564 getType(qualifiedName) {
565
566 const namespace = ModelUtil.getNamespace(qualifiedName);
567
568 const modelFile = this.getModelFile(namespace);
569 if (!modelFile) {
570 const formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns');
571 throw new TypeNotFoundException(qualifiedName, formatter({
572 type: qualifiedName
573 }));
574 }
575
576 const classDecl = modelFile.getType(qualifiedName);
577 if (!classDecl) {
578 const formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns');
579 throw new TypeNotFoundException(qualifiedName, formatter({
580 type: ModelUtil.getShortName(qualifiedName),
581 namespace: namespace
582 }));
583 }
584
585 return classDecl;
586 }
587
588
589 /**
590 * Get all class declarations from system namespaces
591 * @return {ClassDeclaration[]} the ClassDeclarations from system namespaces
592 */
593 getSystemTypes() {
594 return this.getModelFiles()
595 .filter((modelFile) => {
596 return modelFile.isSystemModelFile();
597 })
598 .reduce((classDeclarations, modelFile) => {
599 return classDeclarations.concat(modelFile.getAllDeclarations());
600 }, [])
601 .filter((classDeclaration) => {
602 return classDeclaration.isSystemCoreType();
603 });
604 }
605
606 /**
607 * Get the AssetDeclarations defined in this model manager
608 * @param {Boolean} includeSystemType - Include the decalarations of system type in returned data
609 * @return {AssetDeclaration[]} the AssetDeclarations defined in the model manager
610 */
611 getAssetDeclarations(includeSystemType = true) {
612 return this.getModelFiles().reduce((prev, cur) => {
613 return prev.concat(cur.getAssetDeclarations(includeSystemType));
614 }, []);
615 }
616
617 /**
618 * Get the TransactionDeclarations defined in this model manager
619 * @param {Boolean} includeSystemType - Include the decalarations of system type in returned data
620 * @return {TransactionDeclaration[]} the TransactionDeclarations defined in the model manager
621 */
622 getTransactionDeclarations(includeSystemType = true) {
623 return this.getModelFiles().reduce((prev, cur) => {
624 return prev.concat(cur.getTransactionDeclarations(includeSystemType));
625 }, []);
626 }
627
628 /**
629 * Get the EventDeclarations defined in this model manager
630 * @param {Boolean} includeSystemType - Include the decalarations of system type in returned data
631 * @return {EventDeclaration[]} the EventDeclaration defined in the model manager
632 */
633 getEventDeclarations(includeSystemType = true) {
634 return this.getModelFiles().reduce((prev, cur) => {
635 return prev.concat(cur.getEventDeclarations(includeSystemType));
636 }, []);
637 }
638
639 /**
640 * Get the ParticipantDeclarations defined in this model manager
641 * @param {Boolean} includeSystemType - Include the decalarations of system type in returned data
642 * @return {ParticipantDeclaration[]} the ParticipantDeclaration defined in the model manager
643 */
644 getParticipantDeclarations(includeSystemType = true) {
645 return this.getModelFiles().reduce((prev, cur) => {
646 return prev.concat(cur.getParticipantDeclarations(includeSystemType));
647 }, []);
648 }
649
650 /**
651 * Get the EnumDeclarations defined in this model manager
652 * @param {Boolean} includeSystemType - Include the decalarations of system type in returned data
653 * @return {EnumDeclaration[]} the EnumDeclaration defined in the model manager
654 */
655 getEnumDeclarations(includeSystemType = true) {
656 return this.getModelFiles().reduce((prev, cur) => {
657 return prev.concat(cur.getEnumDeclarations(includeSystemType));
658 }, []);
659 }
660
661 /**
662 * Get the Concepts defined in this model manager
663 * @param {Boolean} includeSystemType - Include the decalarations of system type in returned data
664 * @return {ConceptDeclaration[]} the ConceptDeclaration defined in the model manager
665 */
666 getConceptDeclarations(includeSystemType = true) {
667 return this.getModelFiles().reduce((prev, cur) => {
668 return prev.concat(cur.getConceptDeclarations(includeSystemType));
669 }, []);
670 }
671
672 /**
673 * Get a factory for creating new instances of types defined in this model manager.
674 * @return {Factory} A factory for creating new instances of types defined in this model manager.
675 */
676 getFactory() {
677 return this.factory;
678 }
679
680 /**
681 * Get a serializer for serializing instances of types defined in this model manager.
682 * @return {Serializer} A serializer for serializing instances of types defined in this model manager.
683 */
684 getSerializer() {
685 return this.serializer;
686 }
687
688 /**
689 * Get the decorator factories for this model manager.
690 * @return {DecoratorFactory[]} The decorator factories for this model manager.
691 */
692 getDecoratorFactories() {
693 return this.decoratorFactories;
694 }
695
696 /**
697 * Add a decorator factory to this model manager.
698 * @param {DecoratorFactory} factory The decorator factory to add to this model manager.
699 */
700 addDecoratorFactory(factory) {
701 this.decoratorFactories.push(factory);
702 }
703
704 /**
705 * Alternative instanceof that is reliable across different module instances
706 * @see https://github.com/hyperledger/composer-concerto/issues/47
707 *
708 * @param {object} object - The object to test against
709 * @returns {boolean} - True, if the object is an instance of a ModelManager
710 */
711 static [Symbol.hasInstance](object){
712 return typeof object !== 'undefined' && object !== null && Boolean(object._isModelManager);
713 }
714}
715
716module.exports = ModelManager;