UNPKG

23.2 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.
43 *
44 * Use the {@link Concerto} class to validate instances.
45 *
46 * @class
47 * @memberof module:concerto-core
48 */
49class ModelManager {
50 /**
51 * Create the ModelManager.
52 * @param {object} [options] - Serializer options
53 */
54 constructor(options) {
55 this.modelFiles = {};
56 this.factory = new Factory(this);
57 this.serializer = new Serializer(this.factory, this, options);
58 this.decoratorFactories = [];
59 this._isModelManager = true;
60 this.addRootModel();
61 }
62
63 /**
64 * Adds root types
65 * @private
66 */
67 addRootModel() {
68 this.addModelFile( `namespace concerto
69abstract concept Concept {}
70abstract concept Asset identified {}
71abstract concept Participant identified {}
72abstract concept Transaction {}
73abstract concept Event {}
74`, 'concerto.cto');
75 }
76
77 /**
78 * Visitor design pattern
79 * @param {Object} visitor - the visitor
80 * @param {Object} parameters - the parameter
81 * @return {Object} the result of visiting or null
82 */
83 accept(visitor, parameters) {
84 return visitor.visit(this, parameters);
85 }
86
87 /**
88 * Validates a Concerto file (as a string) to the ModelManager.
89 * Concerto files have a single namespace.
90 *
91 * Note that if there are dependencies between multiple files the files
92 * must be added in dependency order, or the addModelFiles method can be
93 * used to add a set of files irrespective of dependencies.
94 * @param {string} modelFile - The Concerto file as a string
95 * @param {string} [fileName] - a file name to associate with the model file
96 * @throws {IllegalModelException}
97 */
98 validateModelFile(modelFile, fileName) {
99 if (typeof modelFile === 'string') {
100 let m = new ModelFile(this, modelFile, fileName);
101 m.validate();
102 } else {
103 modelFile.validate();
104 }
105 }
106
107 /**
108 * Throws an error with details about the existing namespace.
109 * @param {ModelFile} modelFile The model file that is trying to declare an existing namespace
110 * @private
111 */
112 _throwAlreadyExists(modelFile) {
113 const existingModelFileName = this.modelFiles[modelFile.getNamespace()].getName();
114 const postfix = existingModelFileName ? ` in file ${existingModelFileName}` : '';
115 const prefix = modelFile.getName() ? ` specified in file ${modelFile.getName()}` : '';
116 let errMsg = `Namespace ${modelFile.getNamespace()}${prefix} is already declared${postfix}`;
117 throw new Error(errMsg);
118 }
119
120 /**
121 * Adds a Concerto file (as a string) to the ModelManager.
122 * Concerto files have a single namespace. If a Concerto file with the
123 * same namespace has already been added to the ModelManager then it
124 * will be replaced.
125 * Note that if there are dependencies between multiple files the files
126 * must be added in dependency order, or the addModelFiles method can be
127 * used to add a set of files irrespective of dependencies.
128 * @param {string} modelFile - The Concerto file as a string
129 * @param {string} fileName - an optional file name to associate with the model file
130 * @param {boolean} [disableValidation] - If true then the model files are not validated
131 * @throws {IllegalModelException}
132 * @return {Object} The newly added model file (internal).
133 */
134 addModelFile(modelFile, fileName, disableValidation) {
135 const NAME = 'addModelFile';
136 debug(NAME, 'addModelFile', modelFile, fileName);
137
138 let m = null;
139
140 if (typeof modelFile === 'string') {
141 m = new ModelFile(this, modelFile, fileName);
142 } else {
143 m = modelFile;
144 }
145
146 if (!this.modelFiles[m.getNamespace()]) {
147 if (!disableValidation) {
148 m.validate();
149 }
150 this.modelFiles[m.getNamespace()] = m;
151 } else {
152 this._throwAlreadyExists(m);
153 }
154
155 return m;
156 }
157
158 /**
159 * Updates a Concerto file (as a string) on the ModelManager.
160 * Concerto files have a single namespace. If a Concerto file with the
161 * same namespace has already been added to the ModelManager then it
162 * will be replaced.
163 * @param {string} modelFile - The Concerto file as a string
164 * @param {string} [fileName] - a file name to associate with the model file
165 * @param {boolean} [disableValidation] - If true then the model files are not validated
166 * @throws {IllegalModelException}
167 * @returns {Object} The newly added model file (internal).
168 */
169 updateModelFile(modelFile, fileName, disableValidation) {
170 const NAME = 'updateModelFile';
171 debug(NAME, 'updateModelFile', modelFile, fileName);
172 if (typeof modelFile === 'string') {
173 let m = new ModelFile(this, modelFile, fileName);
174 return this.updateModelFile(m,fileName,disableValidation);
175 } else {
176 let existing = this.modelFiles[modelFile.getNamespace()];
177 if (!existing) {
178 throw new Error(`Model file for namespace ${modelFile.getNamespace()} not found`);
179 }
180 if (!disableValidation) {
181 modelFile.validate();
182 }
183 }
184 this.modelFiles[modelFile.getNamespace()] = modelFile;
185 return modelFile;
186 }
187
188 /**
189 * Remove the Concerto file for a given namespace
190 * @param {string} namespace - The namespace of the model file to delete.
191 */
192 deleteModelFile(namespace) {
193 if (!this.modelFiles[namespace]) {
194 throw new Error('Model file does not exist');
195 } else {
196 delete this.modelFiles[namespace];
197 }
198 }
199
200 /**
201 * Add a set of Concerto files to the model manager.
202 * @param {object[]} modelFiles - An array of Concerto files as strings or ModelFile objects.
203 * @param {string[]} [fileNames] - A array of file names to associate with the model files
204 * @param {boolean} [disableValidation] - If true then the model files are not validated
205 * @returns {Object[]} The newly added model files (internal).
206 */
207 addModelFiles(modelFiles, fileNames, disableValidation) {
208 const NAME = 'addModelFiles';
209 debug(NAME, 'addModelFiles', modelFiles, fileNames);
210 const originalModelFiles = {};
211 Object.assign(originalModelFiles, this.modelFiles);
212 let newModelFiles = [];
213
214 try {
215 // create the model files
216 for (let n = 0; n < modelFiles.length; n++) {
217 const modelFile = modelFiles[n];
218 let fileName = null;
219
220 if (fileNames) {
221 fileName = fileNames[n];
222 }
223
224 const m = typeof modelFile === 'string' ? new ModelFile(this, modelFile, fileName) : modelFile;
225 if (!this.modelFiles[m.getNamespace()]) {
226 this.modelFiles[m.getNamespace()] = m;
227 newModelFiles.push(m);
228 } else {
229 this._throwAlreadyExists(m);
230 }
231 }
232
233 // re-validate all the model files
234 if (!disableValidation) {
235 this.validateModelFiles();
236 }
237
238 // return the model files.
239 return newModelFiles;
240 } catch (err) {
241 this.modelFiles = {};
242 Object.assign(this.modelFiles, originalModelFiles);
243 throw err;
244 } finally {
245 debug(NAME, newModelFiles);
246 }
247 }
248
249
250 /**
251 * Validates all models files in this model manager
252 */
253 validateModelFiles() {
254 for (let ns in this.modelFiles) {
255 this.modelFiles[ns].validate();
256 }
257 }
258
259 /**
260 * Downloads all ModelFiles that are external dependencies and adds or
261 * updates them in this ModelManager.
262 * @param {Object} [options] - Options object passed to ModelFileLoaders
263 * @param {ModelFileDownloader} [modelFileDownloader] - an optional ModelFileDownloader
264 * @throws {IllegalModelException} if the models fail validation
265 * @return {Promise} a promise when the download and update operation is completed.
266 */
267 async updateExternalModels(options, modelFileDownloader) {
268
269 const NAME = 'updateExternalModels';
270 debug(NAME, 'updateExternalModels', options);
271
272 if(!modelFileDownloader) {
273 modelFileDownloader = new ModelFileDownloader(new DefaultModelFileLoader(this));
274 }
275
276 const externalModelFiles = await modelFileDownloader.downloadExternalDependencies(this.getModelFiles(), options)
277 .catch(error => {
278 // If we're not able to download the latest dependencies, see whether the models all validate based on the available cached models.
279 if(error.code === 'MISSING_DEPENDENCY'){
280 try {
281 this.validateModelFiles();
282 return [];
283 } catch (validationError){
284 // The validation error tells us the first model that is missing from the model manager, but the dependency download
285 // will fail at the first external model, regardless of whether there is already a local copy.
286 // As a hint to the user we display the URL of the external model that can't be found.
287 const modelFile = this.getModelFileByFileName(validationError.fileName);
288 const namespaces = modelFile.getExternalImports();
289 const missingNs = Object.keys(namespaces).find((ns) => validationError.shortMessage.includes(ns));
290 const url = modelFile.getImportURI(missingNs);
291 const err = new Error(`Unable to download external model dependency '${url}'`);
292 err.code = 'MISSING_DEPENDENCY';
293 throw err;
294 }
295 } else {
296 throw error;
297 }
298 });
299 const originalModelFiles = {};
300 Object.assign(originalModelFiles, this.modelFiles);
301
302 try {
303 externalModelFiles.forEach((mf) => {
304 const existing = this.modelFiles[mf.getNamespace()];
305
306 if (existing) {
307 this.updateModelFile(mf, mf.getName(), true); // disable validation
308 } else {
309 this.addModelFile(mf, mf.getName(), true); // disable validation
310 }
311 });
312
313 // now everything is applied, we need to revalidate all models
314 this.validateModelFiles();
315 return externalModelFiles;
316 } catch (err) {
317 this.modelFiles = {};
318 Object.assign(this.modelFiles, originalModelFiles);
319 throw err;
320 }
321 }
322
323 /**
324 * Write all models in this model manager to the specified path in the file system
325 *
326 * @param {string} path to a local directory
327 * @param {Object} [options] - Options object
328 * @param {boolean} options.includeExternalModels -
329 * If true, external models are written to the file system. Defaults to true
330 */
331 writeModelsToFileSystem(path, options = {}) {
332 if(!path){
333 throw new Error('`path` is a required parameter of writeModelsToFileSystem');
334 }
335
336 const opts = Object.assign({
337 includeExternalModels: true,
338 }, options);
339
340 this.getModelFiles().forEach(function (file) {
341 if (file.isExternal() && !opts.includeExternalModels) {
342 return;
343 }
344 // Always assume file names have been normalized from `\` to `/`
345 const filename = slash(file.fileName).split('/').pop();
346 fs.writeFileSync(path + fsPath.sep + filename, file.definitions);
347 });
348 }
349
350 /**
351 * Get the array of model file instances
352 * @param {Boolean} [includeConcertoNamespace] - whether to include the concerto namespace
353 * (default to false)
354 * @return {ModelFile[]} The ModelFiles registered
355 * @private
356 */
357 getModelFiles(includeConcertoNamespace) {
358 let keys = Object.keys(this.modelFiles);
359 let result = [];
360
361 for (let n = 0; n < keys.length; n++) {
362 const ns = keys[n];
363 if(includeConcertoNamespace || ns !== 'concerto') {
364 result.push(this.modelFiles[ns]);
365 }
366 }
367
368 return result;
369 }
370
371 /**
372 * Gets all the Concerto models
373 * @param {Object} [options] - Options object
374 * @param {boolean} options.includeExternalModels -
375 * If true, external models are written to the file system. Defaults to true
376 * @return {Array<{name:string, content:string}>} the name and content of each CTO file
377 */
378 getModels(options) {
379 const modelFiles = this.getModelFiles();
380 let models = [];
381 const opts = Object.assign({
382 includeExternalModels: true,
383 }, options);
384
385 modelFiles.forEach(function (file) {
386 if (file.isExternal() && !opts.includeExternalModels) {
387 return;
388 }
389 let fileName;
390 if (file.fileName === 'UNKNOWN' || file.fileName === null || !file.fileName) {
391 fileName = file.namespace + '.cto';
392 } else {
393 let fileIdentifier = file.fileName;
394 fileName = fsPath.basename(fileIdentifier);
395 }
396 models.push({ 'name' : fileName, 'content' : file.definitions });
397 });
398 return models;
399 }
400
401 /**
402 * Check that the type is valid and returns the FQN of the type.
403 * @param {string} context - error reporting context
404 * @param {string} type - fully qualified type name
405 * @return {string} - the resolved type name (fully qualified)
406 * @throws {IllegalModelException} - if the type is not defined
407 * @private
408 */
409 resolveType(context, type) {
410 // is the type a primitive?
411 if (ModelUtil.isPrimitiveType(type)) {
412 return type;
413 }
414
415 let ns = ModelUtil.getNamespace(type);
416 let modelFile = this.getModelFile(ns);
417 if (!modelFile) {
418 let formatter = Globalize.messageFormatter('modelmanager-resolvetype-nonsfortype');
419 throw new IllegalModelException(formatter({
420 type: type,
421 context: context
422 }));
423 }
424
425 if (modelFile.isLocalType(type)) {
426 return type;
427 }
428
429 let formatter = Globalize.messageFormatter('modelmanager-resolvetype-notypeinnsforcontext');
430 throw new IllegalModelException(formatter({
431 context: context,
432 type: type,
433 namespace: modelFile.getNamespace()
434 }));
435 }
436
437 /**
438 * Remove all registered Concerto files
439 */
440 clearModelFiles() {
441 this.modelFiles = {};
442 this.addRootModel();
443 }
444
445 /**
446 * Get the ModelFile associated with a namespace
447 *
448 * @param {string} namespace - the namespace containing the ModelFile
449 * @return {ModelFile} registered ModelFile for the namespace or null
450 */
451 getModelFile(namespace) {
452 return this.modelFiles[namespace];
453 }
454
455 /**
456 * Get the ModelFile associated with a file name
457 *
458 * @param {string} fileName - the fileName associated with the ModelFile
459 * @return {ModelFile} registered ModelFile for the namespace or null
460 * @private
461 */
462 getModelFileByFileName(fileName) {
463 return this.getModelFiles().filter(mf => mf.getName() === fileName)[0];
464 }
465
466 /**
467 * Get the namespaces registered with the ModelManager.
468 * @return {string[]} namespaces - the namespaces that have been registered.
469 */
470 getNamespaces() {
471 return Object.keys(this.modelFiles);
472 }
473
474 /**
475 * Look up a type in all registered namespaces.
476 *
477 * @param {string} qualifiedName - fully qualified type name.
478 * @return {ClassDeclaration} - the class declaration for the specified type.
479 * @throws {TypeNotFoundException} - if the type cannot be found or is a primitive type.
480 * @private
481 */
482 getType(qualifiedName) {
483
484 const namespace = ModelUtil.getNamespace(qualifiedName);
485
486 const modelFile = this.getModelFile(namespace);
487 if (!modelFile) {
488 const formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns');
489 throw new TypeNotFoundException(qualifiedName, formatter({
490 type: qualifiedName
491 }));
492 }
493
494 const classDecl = modelFile.getType(qualifiedName);
495 if (!classDecl) {
496 const formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns');
497 throw new TypeNotFoundException(qualifiedName, formatter({
498 type: ModelUtil.getShortName(qualifiedName),
499 namespace: namespace
500 }));
501 }
502
503 return classDecl;
504 }
505
506 /**
507 * Get the AssetDeclarations defined in this model manager
508 * @return {AssetDeclaration[]} the AssetDeclarations defined in the model manager
509 */
510 getAssetDeclarations() {
511 return this.getModelFiles().reduce((prev, cur) => {
512 return prev.concat(cur.getAssetDeclarations());
513 }, []);
514 }
515
516 /**
517 * Get the TransactionDeclarations defined in this model manager
518 * @return {TransactionDeclaration[]} the TransactionDeclarations defined in the model manager
519 */
520 getTransactionDeclarations() {
521 return this.getModelFiles().reduce((prev, cur) => {
522 return prev.concat(cur.getTransactionDeclarations());
523 }, []);
524 }
525
526 /**
527 * Get the EventDeclarations defined in this model manager
528 * @return {EventDeclaration[]} the EventDeclaration defined in the model manager
529 */
530 getEventDeclarations() {
531 return this.getModelFiles().reduce((prev, cur) => {
532 return prev.concat(cur.getEventDeclarations());
533 }, []);
534 }
535
536 /**
537 * Get the ParticipantDeclarations defined in this model manager
538 * @return {ParticipantDeclaration[]} the ParticipantDeclaration defined in the model manager
539 */
540 getParticipantDeclarations() {
541 return this.getModelFiles().reduce((prev, cur) => {
542 return prev.concat(cur.getParticipantDeclarations());
543 }, []);
544 }
545
546 /**
547 * Get the EnumDeclarations defined in this model manager
548 * @return {EnumDeclaration[]} the EnumDeclaration defined in the model manager
549 */
550 getEnumDeclarations() {
551 return this.getModelFiles().reduce((prev, cur) => {
552 return prev.concat(cur.getEnumDeclarations());
553 }, []);
554 }
555
556 /**
557 * Get the Concepts defined in this model manager
558 * @return {ConceptDeclaration[]} the ConceptDeclaration defined in the model manager
559 */
560 getConceptDeclarations() {
561 return this.getModelFiles().reduce((prev, cur) => {
562 return prev.concat(cur.getConceptDeclarations());
563 }, []);
564 }
565
566 /**
567 * Get a factory for creating new instances of types defined in this model manager.
568 * @return {Factory} A factory for creating new instances of types defined in this model manager.
569 */
570 getFactory() {
571 return this.factory;
572 }
573
574 /**
575 * Get a serializer for serializing instances of types defined in this model manager.
576 * @return {Serializer} A serializer for serializing instances of types defined in this model manager.
577 */
578 getSerializer() {
579 return this.serializer;
580 }
581
582 /**
583 * Get the decorator factories for this model manager.
584 * @return {DecoratorFactory[]} The decorator factories for this model manager.
585 */
586 getDecoratorFactories() {
587 return this.decoratorFactories;
588 }
589
590 /**
591 * Add a decorator factory to this model manager.
592 * @param {DecoratorFactory} factory The decorator factory to add to this model manager.
593 */
594 addDecoratorFactory(factory) {
595 this.decoratorFactories.push(factory);
596 }
597
598 /**
599 * Checks if this fully qualified type name is derived from another.
600 * @param {string} fqt1 The fully qualified type name to check.
601 * @param {string} fqt2 The fully qualified type name it is may be derived from.
602 * @returns {boolean} True if this instance is an instance of the specified fully
603 * qualified type name, false otherwise.
604 */
605 derivesFrom(fqt1, fqt2) {
606 // Check to see if this is an exact instance of the specified type.
607 let typeDeclaration = this.getType(fqt1);
608 while (typeDeclaration) {
609 if (typeDeclaration.getFullyQualifiedName() === fqt2) {
610 return true;
611 }
612 typeDeclaration = typeDeclaration.getSuperTypeDeclaration();
613 }
614 return false;
615 }
616
617 /**
618 * Alternative instanceof that is reliable across different module instances
619 * @see https://github.com/hyperledger/composer-concerto/issues/47
620 *
621 * @param {object} object - The object to test against
622 * @returns {boolean} - True, if the object is an instance of a ModelManager
623 */
624 static [Symbol.hasInstance](object){
625 return typeof object !== 'undefined' && object !== null && Boolean(object._isModelManager);
626 }
627}
628
629module.exports = ModelManager;