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 | ;
|
16 |
|
17 | const Decorated = require('./decorated');
|
18 | const EnumValueDeclaration = require('./enumvaluedeclaration');
|
19 | const Field = require('./field');
|
20 | const Globalize = require('../globalize');
|
21 | const IllegalModelException = require('./illegalmodelexception');
|
22 | const Introspector = require('./introspector');
|
23 | const ModelUtil = require('../modelutil');
|
24 | const RelationshipDeclaration = require('./relationshipdeclaration');
|
25 |
|
26 | /**
|
27 | * ClassDeclaration defines the structure (model/schema) of composite data.
|
28 | * It is composed of a set of Properties, may have an identifying field, and may
|
29 | * have a super-type.
|
30 | * A ClassDeclaration is conceptually owned by a ModelFile which
|
31 | * defines all the classes that are part of a namespace.
|
32 | *
|
33 | *
|
34 | * @abstract
|
35 | * @class
|
36 | * @memberof module:concerto-core
|
37 | */
|
38 | class ClassDeclaration extends Decorated {
|
39 |
|
40 | /**
|
41 | * Create a ClassDeclaration from an Abstract Syntax Tree. The AST is the
|
42 | * result of parsing.
|
43 | *
|
44 | * @param {ModelFile} modelFile - the ModelFile for this class
|
45 | * @param {string} ast - the AST created by the parser
|
46 | * @throws {IllegalModelException}
|
47 | */
|
48 | constructor(modelFile, ast) {
|
49 | super(modelFile, ast);
|
50 | this.process();
|
51 | this.fqn = ModelUtil.getFullyQualifiedName(this.modelFile.getNamespace(), this.name);
|
52 | this._isClassDeclaration = true;
|
53 | }
|
54 |
|
55 | /**
|
56 | * Process the AST and build the model
|
57 | *
|
58 | * @throws {IllegalModelException}
|
59 | * @private
|
60 | */
|
61 | process() {
|
62 |
|
63 | super.process();
|
64 |
|
65 | this.name = this.ast.id.name;
|
66 | this.properties = [];
|
67 | this.superType = null;
|
68 | this.superTypeDeclaration = null;
|
69 | this.idField = null;
|
70 | this.abstract = false;
|
71 |
|
72 | if (this.ast.abstract) {
|
73 | this.abstract = true;
|
74 | }
|
75 |
|
76 | if (this.ast.classExtension) {
|
77 | this.superType = this.ast.classExtension.class.name;
|
78 | } else {
|
79 | // if we are not a system type, then we should set the
|
80 | // super type to the system type for this class declaration
|
81 | if (!this.isSystemCoreType()) {
|
82 | this.superType = this.getSystemType();
|
83 | }
|
84 | }
|
85 |
|
86 | if (this.ast.idField) {
|
87 | this.idField = this.ast.idField.name;
|
88 | }
|
89 |
|
90 | for (let n = 0; n < this.ast.body.declarations.length; n++) {
|
91 | let thing = this.ast.body.declarations[n];
|
92 |
|
93 | if (thing.type === 'FieldDeclaration') {
|
94 | this.properties.push(new Field(this, thing));
|
95 | } else if (thing.type === 'RelationshipDeclaration') {
|
96 | this.properties.push(new RelationshipDeclaration(this, thing));
|
97 | } else if (thing.type === 'EnumPropertyDeclaration') {
|
98 | this.properties.push(new EnumValueDeclaration(this, thing));
|
99 | } else {
|
100 | let formatter = Globalize.messageFormatter('classdeclaration-process-unrecmodelelem');
|
101 | throw new IllegalModelException(formatter({
|
102 | 'type': thing.type
|
103 | }), this.modelFile, this.ast.location);
|
104 | }
|
105 | }
|
106 | }
|
107 |
|
108 | /**
|
109 | * Adds a required field named 'timestamp' of type 'DateTime' if this class declaration does not have a super type.
|
110 | * This method should only be called by system code.
|
111 | * @private
|
112 | */
|
113 | addTimestampField() {
|
114 | // add a timestamp field
|
115 | if(this.superType === null) {
|
116 | const definition = {};
|
117 | definition.id = {};
|
118 | definition.id.name = 'timestamp';
|
119 | definition.propertyType = {};
|
120 | definition.propertyType.name = 'DateTime';
|
121 | this.properties.push(new Field(this, definition));
|
122 | }
|
123 | }
|
124 |
|
125 | /**
|
126 | * Resolve the super type on this class and store it as an internal property.
|
127 | * @return {ClassDeclaration} The super type, or null if non specified.
|
128 | */
|
129 | _resolveSuperType() {
|
130 | if (!this.superType) {
|
131 | return null;
|
132 | }
|
133 | // Clear out any old resolved super types.
|
134 | this.superTypeDeclaration = null;
|
135 | let classDecl = null;
|
136 | if (this.getModelFile().isImportedType(this.superType)) {
|
137 | let fqnSuper = this.getModelFile().resolveImport(this.superType);
|
138 | classDecl = this.modelFile.getModelManager().getType(fqnSuper);
|
139 | } else {
|
140 | classDecl = this.getModelFile().getType(this.superType);
|
141 | }
|
142 |
|
143 | if (!classDecl) {
|
144 | throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location);
|
145 | }
|
146 |
|
147 | // Prevent extending declaration with different type of declaration
|
148 | if (this.constructor.name !== classDecl.constructor.name) {
|
149 | let typeName = this.getSystemType();
|
150 | let superTypeName = classDecl.getSystemType();
|
151 | throw new IllegalModelException(`${typeName} (${this.getName()}) cannot extend ${superTypeName} (${classDecl.getName()})`, this.modelFile, this.ast.location);
|
152 | }
|
153 | this.superTypeDeclaration = classDecl;
|
154 | return classDecl;
|
155 | }
|
156 |
|
157 | /**
|
158 | * Semantic validation of the structure of this class. Subclasses should
|
159 | * override this method to impose additional semantic constraints on the
|
160 | * contents/relations of fields.
|
161 | *
|
162 | * @throws {IllegalModelException}
|
163 | * @private
|
164 | */
|
165 | validate() {
|
166 |
|
167 | super.validate();
|
168 |
|
169 | const declarations = this.getModelFile().getAllDeclarations();
|
170 | for (let n = 0; n < declarations.length; n++) {
|
171 | let declaration = declarations[n];
|
172 |
|
173 | // check we don't have an asset with the same name
|
174 | for (let i = n + 1; i < declarations.length; i++) {
|
175 | let otherDeclaration = declarations[i];
|
176 | if (declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) {
|
177 | throw new IllegalModelException(`Duplicate class name ${declaration.getName()}`);
|
178 | }
|
179 | }
|
180 | }
|
181 |
|
182 | // TODO (LG) check that all imported classes exist, rather than just
|
183 | // used imports
|
184 |
|
185 | // if we have a super type make sure it exists
|
186 | if (this.superType !== null) {
|
187 | this._resolveSuperType();
|
188 | }
|
189 |
|
190 | if (this.idField) {
|
191 | const idField = this.getProperty(this.idField);
|
192 | if (!idField) {
|
193 | let formatter = Globalize('en').messageFormatter('classdeclaration-validate-identifiernotproperty');
|
194 | throw new IllegalModelException(formatter({
|
195 | 'class': this.name,
|
196 | 'idField': this.idField
|
197 | }), this.modelFile, this.ast.location);
|
198 | } else {
|
199 | // check that identifiers are strings
|
200 | if (idField.getType() !== 'String') {
|
201 | let formatter = Globalize('en').messageFormatter('classdeclaration-validate-identifiernotstring');
|
202 | throw new IllegalModelException(formatter({
|
203 | 'class': this.name,
|
204 | 'idField': this.idField
|
205 | }), this.modelFile, this.ast.location);
|
206 | }
|
207 |
|
208 | if (idField.isOptional()) {
|
209 | throw new IllegalModelException('Identifying fields cannot be optional.', this.modelFile, this.ast.location);
|
210 | }
|
211 | if (this.getSuperType()) {
|
212 | // check this class doesn't declare the identifying field as a property.
|
213 | if (idField.getName() === this.getModelFile().getType(this.superType).getIdentifierFieldName()) {
|
214 | throw new IllegalModelException('Identifier from super class cannot be redeclared.', this.modelFile, this.ast.location);
|
215 | }
|
216 |
|
217 | // TODO: This has been disabled pending major version bump and/or confirmation that this is illegal
|
218 | // As this class has an idField declared, check the superclass doesn't
|
219 | //if (this.getModelFile().getType(this.superType).getIdentifierFieldName()) {
|
220 | // throw new IllegalModelException('Identifier defined in super class, identifiers cannot be overridden', this.modelFile, this.ast.location);
|
221 | //}
|
222 | }
|
223 | }
|
224 | } else {
|
225 | if (this.isAbstract() === false && this.isEnum() === false && this.isConcept() === false) {
|
226 | if (this.getIdentifierFieldName() === null) {
|
227 | let formatter = Globalize('en').messageFormatter('classdeclaration-validate-missingidentifier');
|
228 | throw new IllegalModelException(formatter({
|
229 | 'class': this.name
|
230 | }), this.modelFile, this.ast.location);
|
231 | }
|
232 | }
|
233 | }
|
234 |
|
235 | // we also have to check fields defined in super classes
|
236 | const properties = this.getProperties();
|
237 | for (let n = 0; n < properties.length; n++) {
|
238 | let field = properties[n];
|
239 |
|
240 | // check we don't have a field with the same name
|
241 | for (let i = n + 1; i < properties.length; i++) {
|
242 | let otherField = properties[i];
|
243 | if (field.getName() === otherField.getName()) {
|
244 | let formatter = Globalize('en').messageFormatter('classdeclaration-validate-duplicatefieldname');
|
245 | throw new IllegalModelException(formatter({
|
246 | 'class': this.name,
|
247 | 'fieldName': field.getName()
|
248 | }), this.modelFile, this.ast.location);
|
249 | }
|
250 | }
|
251 |
|
252 | // we now validate the field, however to ensure that
|
253 | // imports are resolved correctly we validate in the context
|
254 | // of the declared type of the field for non-primitives in a different namespace
|
255 | if (field.isPrimitive() || this.isEnum() || field.getNamespace() === this.getNamespace()) {
|
256 | field.validate(this);
|
257 | } else {
|
258 | const typeFqn = field.getFullyQualifiedTypeName();
|
259 | const classDecl = this.modelFile.getModelManager().getType(typeFqn);
|
260 | field.validate(classDecl);
|
261 | }
|
262 | }
|
263 | }
|
264 |
|
265 | /**
|
266 | * Returns the base system type for this type of class declaration. Override
|
267 | * this method in derived classes to specify a base system type.
|
268 | *
|
269 | * @return {string} the short name of the base system type or null
|
270 | */
|
271 | getSystemType() {
|
272 | return null;
|
273 | }
|
274 |
|
275 | /**
|
276 | * Returns true if this class is declared as abstract in the model file
|
277 | *
|
278 | * @return {boolean} true if the class is abstract
|
279 | */
|
280 | isAbstract() {
|
281 | return this.abstract;
|
282 | }
|
283 |
|
284 | /**
|
285 | * Returns true if this class is an enumeration.
|
286 | *
|
287 | * @return {boolean} true if the class is an enumerated type
|
288 | */
|
289 | isEnum() {
|
290 | return false;
|
291 | }
|
292 |
|
293 | /**
|
294 | * Returns true if this class is the definition of a concept.
|
295 | *
|
296 | * @return {boolean} true if the class is a concept
|
297 | */
|
298 | isConcept() {
|
299 | return false;
|
300 | }
|
301 |
|
302 | /**
|
303 | * Returns true if this class is the definition of an event.
|
304 | *
|
305 | * @return {boolean} true if the class is an event
|
306 | */
|
307 | isEvent() {
|
308 | return false;
|
309 | }
|
310 |
|
311 | /**
|
312 | * Returns true if this class can be pointed to by a relationship
|
313 | *
|
314 | * @return {boolean} true if the class may be pointed to by a relationship
|
315 | */
|
316 | isRelationshipTarget() {
|
317 | return false;
|
318 | }
|
319 |
|
320 | /**
|
321 | * Returns true if this class can be pointed to by a relationship in a
|
322 | * system model
|
323 | *
|
324 | * @return {boolean} true if the class may be pointed to by a relationship
|
325 | */
|
326 | isSystemRelationshipTarget() {
|
327 | return this.isRelationshipTarget();
|
328 | }
|
329 |
|
330 | /**
|
331 | * Returns true is this type is in the system namespace
|
332 | *
|
333 | * @return {boolean} true if the class may be pointed to by a relationship
|
334 | */
|
335 | isSystemType() {
|
336 | return this.modelFile.isSystemModelFile();
|
337 | }
|
338 |
|
339 | /**
|
340 | * Returns true if this class is a system core type - both in the system
|
341 | * namespace, and also one of the system core types (Asset, Participant, etc).
|
342 | *
|
343 | * @return {boolean} true if the class may be pointed to by a relationship
|
344 | */
|
345 | isSystemCoreType() {
|
346 | return this.isSystemType() &&
|
347 | this.getSystemType() === this.getName();
|
348 | }
|
349 |
|
350 | /**
|
351 | * Returns the short name of a class. This name does not include the
|
352 | * namespace from the owning ModelFile.
|
353 | *
|
354 | * @return {string} the short name of this class
|
355 | */
|
356 | getName() {
|
357 | return this.name;
|
358 | }
|
359 |
|
360 | /**
|
361 | * Return the namespace of this class.
|
362 | * @return {String} namespace - a namespace.
|
363 | */
|
364 | getNamespace() {
|
365 | return this.modelFile.getNamespace();
|
366 | }
|
367 |
|
368 | /**
|
369 | * Returns the fully qualified name of this class.
|
370 | * The name will include the namespace if present.
|
371 | *
|
372 | * @return {string} the fully-qualified name of this class
|
373 | */
|
374 | getFullyQualifiedName() {
|
375 | return this.fqn;
|
376 | }
|
377 |
|
378 | /**
|
379 | * Returns the name of the identifying field for this class. Note
|
380 | * that the identifying field may come from a super type.
|
381 | *
|
382 | * @return {string} the name of the id field for this class
|
383 | */
|
384 | getIdentifierFieldName() {
|
385 |
|
386 | if (this.idField) {
|
387 | return this.idField;
|
388 | } else {
|
389 | if (this.getSuperType()) {
|
390 | // we first check our own modelfile, as we may be called from validate
|
391 | // in which case our model file has not yet been added to the model modelManager
|
392 | let classDecl = this.getModelFile().getLocalType(this.getSuperType());
|
393 |
|
394 | // not a local type, so we try the model manager
|
395 | if (!classDecl) {
|
396 | classDecl = this.modelFile.getModelManager().getType(this.getSuperType());
|
397 | }
|
398 | return classDecl.getIdentifierFieldName();
|
399 | } else {
|
400 | return null;
|
401 | }
|
402 | }
|
403 | }
|
404 |
|
405 | /**
|
406 | * Returns the field with a given name or null if it does not exist.
|
407 | * The field must be directly owned by this class -- the super-type is
|
408 | * not introspected.
|
409 | *
|
410 | * @param {string} name the name of the field
|
411 | * @return {Property} the field definition or null if it does not exist.
|
412 | */
|
413 | getOwnProperty(name) {
|
414 | for (let n = 0; n < this.properties.length; n++) {
|
415 | const field = this.properties[n];
|
416 | if (field.getName() === name) {
|
417 | return field;
|
418 | }
|
419 | }
|
420 |
|
421 | return null;
|
422 | }
|
423 |
|
424 | /**
|
425 | * Returns the fields directly defined by this class.
|
426 | *
|
427 | * @return {Property[]} the array of fields
|
428 | */
|
429 | getOwnProperties() {
|
430 | return this.properties;
|
431 | }
|
432 |
|
433 | /**
|
434 | * Returns the FQN of the super type for this class or null if this
|
435 | * class does not have a super type.
|
436 | *
|
437 | * @return {string} the FQN name of the super type or null
|
438 | */
|
439 | getSuperType() {
|
440 | const superTypeDeclaration = this.getSuperTypeDeclaration();
|
441 | if (superTypeDeclaration) {
|
442 | return superTypeDeclaration.getFullyQualifiedName();
|
443 | } else {
|
444 | return null;
|
445 | }
|
446 | }
|
447 |
|
448 | /**
|
449 | * Get the super type class declaration for this class.
|
450 | * @return {ClassDeclaration} the super type declaration, or null if there is no super type.
|
451 | */
|
452 | getSuperTypeDeclaration() {
|
453 | if (!this.superType) {
|
454 | // No super type.
|
455 | return null;
|
456 | } else if (!this.superTypeDeclaration) {
|
457 | // Super type that hasn't been resolved yet.
|
458 | return this._resolveSuperType();
|
459 | } else {
|
460 | // Resolved super type.
|
461 | return this.superTypeDeclaration;
|
462 | }
|
463 | }
|
464 |
|
465 | /**
|
466 | * Get the class declarations for all subclasses of this class, including this class.
|
467 | * @return {ClassDeclaration[]} subclass declarations.
|
468 | */
|
469 | getAssignableClassDeclarations() {
|
470 | const results = new Set();
|
471 | const modelManager = this.getModelFile().getModelManager();
|
472 | const introspector = new Introspector(modelManager);
|
473 | const allClassDeclarations = introspector.getClassDeclarations();
|
474 | const subclassMap = new Map();
|
475 |
|
476 | // Build map of all direct subclasses relationships
|
477 | allClassDeclarations.forEach((declaration) => {
|
478 | const superType = declaration.getSuperType();
|
479 | if (superType) {
|
480 | const subclasses = subclassMap.get(superType) || new Set();
|
481 | subclasses.add(declaration);
|
482 | subclassMap.set(superType, subclasses);
|
483 | }
|
484 | });
|
485 |
|
486 | // Recursive function to collect all direct and indirect subclasses of a given (set) of base classes.
|
487 | const collectSubclasses = (superclasses) => {
|
488 | superclasses.forEach((declaration) => {
|
489 | results.add(declaration);
|
490 | const superType = declaration.getFullyQualifiedName();
|
491 | const subclasses = subclassMap.get(superType);
|
492 | if (subclasses) {
|
493 | collectSubclasses(subclasses);
|
494 | }
|
495 | });
|
496 | };
|
497 |
|
498 | collectSubclasses([this]);
|
499 |
|
500 | return Array.from(results);
|
501 | }
|
502 |
|
503 | /**
|
504 | * Get all the super-type declarations for this type.
|
505 | * @return {ClassDeclaration[]} super-type declarations.
|
506 | */
|
507 | getAllSuperTypeDeclarations() {
|
508 | const results = [];
|
509 | for (let type = this;
|
510 | (type = type.getSuperTypeDeclaration());) {
|
511 | results.push(type);
|
512 | }
|
513 |
|
514 | return results;
|
515 | }
|
516 |
|
517 | /**
|
518 | * Returns the property with a given name or null if it does not exist.
|
519 | * Fields defined in super-types are also introspected.
|
520 | *
|
521 | * @param {string} name the name of the field
|
522 | * @return {Property} the field, or null if it does not exist
|
523 | */
|
524 | getProperty(name) {
|
525 | let result = this.getOwnProperty(name);
|
526 | let classDecl = null;
|
527 |
|
528 | if (result === null && this.superType !== null) {
|
529 | if (this.getModelFile().isImportedType(this.superType)) {
|
530 | let fqnSuper = this.getModelFile().resolveImport(this.superType);
|
531 | classDecl = this.modelFile.getModelManager().getType(fqnSuper);
|
532 | } else {
|
533 | classDecl = this.getModelFile().getType(this.superType);
|
534 | }
|
535 | result = classDecl.getProperty(name);
|
536 | }
|
537 |
|
538 | return result;
|
539 | }
|
540 |
|
541 | /**
|
542 | * Returns the properties defined in this class and all super classes.
|
543 | *
|
544 | * @return {Property[]} the array of fields
|
545 | */
|
546 | getProperties() {
|
547 | let result = this.getOwnProperties();
|
548 | let classDecl = null;
|
549 | if (this.superType !== null) {
|
550 | if (this.getModelFile().isImportedType(this.superType)) {
|
551 | let fqnSuper = this.getModelFile().resolveImport(this.superType);
|
552 | classDecl = this.modelFile.getModelManager().getType(fqnSuper);
|
553 | } else {
|
554 | classDecl = this.getModelFile().getType(this.superType);
|
555 | }
|
556 |
|
557 | if (classDecl === null) {
|
558 | throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location);
|
559 | }
|
560 |
|
561 | // go get the fields from the super type
|
562 | result = result.concat(classDecl.getProperties());
|
563 | } else {
|
564 | // console.log('No super type for ' + this.getName() );
|
565 | }
|
566 |
|
567 | return result;
|
568 | }
|
569 |
|
570 | /**
|
571 | * Get a nested property using a dotted property path
|
572 | * @param {string} propertyPath The property name or name with nested structure e.g a.b.c
|
573 | * @returns {Property} the property
|
574 | * @throws {IllegalModelException} if the property path is invalid or the property does not exist
|
575 | */
|
576 | getNestedProperty(propertyPath) {
|
577 |
|
578 | const propertyNames = propertyPath.split('.');
|
579 | let classDeclaration = this;
|
580 | let result = null;
|
581 |
|
582 | for (let n = 0; n < propertyNames.length; n++) {
|
583 |
|
584 | // get the nth property
|
585 | result = classDeclaration.getProperty(propertyNames[n]);
|
586 |
|
587 | if (result === null) {
|
588 | throw new IllegalModelException('Property ' + propertyNames[n] + ' does not exist on ' + classDeclaration.getFullyQualifiedName(), this.modelFile, this.ast.location);
|
589 | }
|
590 | // not the last element, get the class of the element
|
591 | else if (n < propertyNames.length - 1) {
|
592 | if (result.isPrimitive() || result.isTypeEnum()) {
|
593 | throw new Error('Property ' + propertyNames[n] + ' is a primitive or enum. Invalid property path: ' + propertyPath);
|
594 | } else {
|
595 | // get the nested type, this throws if the type is missing or if the type is an enum
|
596 | classDeclaration = classDeclaration.getModelFile().getModelManager().getType(result.getFullyQualifiedTypeName());
|
597 | }
|
598 | }
|
599 | }
|
600 |
|
601 | return result;
|
602 | }
|
603 |
|
604 | /**
|
605 | * Returns the string representation of this class
|
606 | * @return {String} the string representation of the class
|
607 | */
|
608 | toString() {
|
609 | let superType = '';
|
610 | if (this.superType) {
|
611 | superType = ' super=' + this.superType;
|
612 | }
|
613 | return 'ClassDeclaration {id=' + this.getFullyQualifiedName() + superType + ' enum=' + this.isEnum() + ' abstract=' + this.isAbstract() + '}';
|
614 | }
|
615 |
|
616 | /**
|
617 | * Alternative instanceof that is reliable across different module instances
|
618 | * @see https://github.com/hyperledger/composer-concerto/issues/47
|
619 | *
|
620 | * @param {object} object - The object to test against
|
621 | * @returns {boolean} - True, if the object is an instance of a Class Declaration
|
622 | */
|
623 | static [Symbol.hasInstance](object){
|
624 | return typeof object !== 'undefined' && object !== null && Boolean(object._isClassDeclaration);
|
625 | }
|
626 |
|
627 | }
|
628 |
|
629 | module.exports = ClassDeclaration;
|