/* * 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 AssetDeclaration = require('composer-common').AssetDeclaration; const Logger = require('composer-common').Logger; const ParticipantDeclaration = require('composer-common').ParticipantDeclaration; const Relationship = require('composer-common').Relationship; const Resource = require('composer-common').Resource; const TransactionDeclaration = require('composer-common').TransactionDeclaration; const LOG = Logger.getLog('Context'); /** * A class for resolving resources and their relationships to other resources. * @protected * @abstract * @memberof module:composer-runtime */ class Resolver { /** * Constructor. * @param {Introspector} introspector The introspector to use. * @param {RegistryManager} registryManager The registry manager to use. */ constructor(introspector, registryManager) { const method = 'constructor'; LOG.entry(method, registryManager); this.introspector = introspector; this.registryManager = registryManager; LOG.exit(method); } /** * Resolve the specified resource or relationship and all of its relationships. * @param {Resource|Relationship} identifiable The identifiable to resolve. * @return {Promise} A promise that is resolved with the resolved {@link Resource} * object when the resource is resolved, or rejected with an error. */ resolve(identifiable) { const method = 'resolve'; LOG.entry(method, identifiable.toString()); let resolveState = { cachedResources: new Map() }; if (identifiable instanceof Resource) { resolveState.cachedResources.set(identifiable.getFullyQualifiedIdentifier(), identifiable); return this.resolveResource(identifiable, resolveState) .then((result) => { LOG.exit(method, result.toString()); return result; }); } else if (identifiable instanceof Relationship) { return this.resolveRelationship(identifiable, resolveState) .then((result) => { LOG.exit(method, result.toString()); return result; }); } else { LOG.error(method, 'unsupported type for identifiable'); throw new Error('unsupported type for identifiable'); } } /** * Resolve the specified resource. * @private * @param {Resource} resource The resource to resolve. * @param {Object} resolveState The current resolve state. * @param {Map} resolveState.cachedResources The cache of resolved resources. * @return {Promise} A promise that is resolved with a {@link Resource} object, * or rejected with an error. */ resolveResource(resource, resolveState) { const method = 'resolveResource'; LOG.entry(method, resource.toString(), resolveState); let classDeclaration = resource.getClassDeclaration(); return classDeclaration.getProperties().reduce((result, property) => { // Get the property value. LOG.debug(method, 'Looking at property', property.getName()); let value = resource[property.getName()]; if (value instanceof Resource) { // Replace the property value with the resolved resource. LOG.debug(method, 'Property value is a resource, resolving', value.toString()); return result.then(() => { return this.resolveResource(value, resolveState); }).then((newValue) => { resource[property.getName()] = newValue; }); } else if (value instanceof Relationship) { // Replace the property value with the resolved relationship. LOG.debug(method, 'Property value is a relationship, resolving', value.toString()); return result.then(() => { return this.resolveRelationship(value, resolveState); }).then((newValue) => { resource[property.getName()] = newValue; }); } else if (Array.isArray(value)) { // Go through each item in the array. LOG.debug(method, 'Property value is an array, iterating over values', value.length); return value.reduce((result, item, index) => { // Handle the array item. if (item instanceof Resource) { // Replace the property value with the resolved resource. LOG.debug(method, 'Array item is a resource, resolving', item.toString()); return result.then(() => { return this.resolveResource(item, resolveState); }).then((newItem) => { value[index] = newItem; }); } else if (item instanceof Relationship) { // Replace the property value with the resolved relationship. LOG.debug(method, 'Property value is a relationship, resolving', item.toString()); return result.then(() => { return this.resolveRelationship(item, resolveState); }).then((newItem) => { value[index] = newItem; }); } else { LOG.debug(method, 'Array item is neither a resource or a relationship, ignoring', item); return result; } }, Promise.resolve()); } else { LOG.debug(method, 'Property value is neither a resource or a relationship, ignoring', value); return result; } }, Promise.resolve()) .then(() => { LOG.exit(method, resource.toString()); return resource; }); } /** * Resolve the specified relationship. * @private * @param {Relationship} relationship The relationship to resolve. * @param {Object} resolveState The current resolve state. * @param {Map} resolveState.cachedResources The cache of resolved resources. * @param {boolean} [resolveState.skipRecursion] Set to true to skip resolving the resolved resource. * @return {Promise} A promise that is resolved with a {@link Resource} object, * or rejected with an error. */ resolveRelationship(relationship, resolveState) { const method = 'resolveRelationship'; LOG.entry(method, relationship.toString(), resolveState); let fqi = relationship.getFullyQualifiedIdentifier(); if (resolveState.cachedResources.has(fqi)) { LOG.debug(method, 'Target resource is already present in cache', fqi); let resource = resolveState.cachedResources.get(fqi); LOG.exit(method, resource.toString()); return Promise.resolve(resource); } let registryId = relationship.getFullyQualifiedType(); let classDeclaration = this.introspector.getClassDeclaration(registryId); LOG.debug(method, 'Got class declaration', classDeclaration); let classType; if (classDeclaration instanceof AssetDeclaration) { classType = 'Asset'; } else if (classDeclaration instanceof ParticipantDeclaration) { classType = 'Participant'; } else if (classDeclaration instanceof TransactionDeclaration) { classType = 'Transaction'; // Special case for this one! registryId = 'default'; } else { throw new Error('Unsupported class declaration type ' + classDeclaration.toString()); } LOG.debug(method, 'Getting registry', registryId); return this.registryManager.get(classType, registryId) .then((registry) => { let resourceId = relationship.getIdentifier(); LOG.debug(method, 'Getting resource in registry', resourceId); return registry.get(resourceId); }) .then((resource) => { LOG.debug(method, 'Got resource from registry, adding to cache'); resolveState.cachedResources.set(fqi, resource); if (resolveState.skipRecursion) { LOG.debug(method, 'Got resource from registry, but skipping resolve'); return resource; } else { LOG.debug(method, 'Got resource from registry, resolving'); return this.resolveResource(resource, resolveState); } }) .then((resource) => { LOG.exit(method, resource.toString()); return resource; }); } /** * Stop serialization of this object. * @return {Object} An empty object. */ toJSON() { return {}; } } module.exports = Resolver;