Source: composer-runtime/lib/context.js

/*
 * 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 AccessController = require('./accesscontroller');
const Api = require('./api');
const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition;
const IdentityManager = require('./identitymanager');
const JSTransactionExecutor = require('./jstransactionexecutor');
const Logger = require('composer-common').Logger;
const LRU = require('lru-cache');
const QueryExecutor = require('./queryexecutor');
const RegistryManager = require('./registrymanager');
const Resolver = require('./resolver');
const TransactionLogger = require('./transactionlogger');

const LOG = Logger.getLog('Context');

const businessNetworkCache = LRU(8);

/**
 * A class representing the current request being handled by the JavaScript engine.
 * @protected
 * @abstract
 * @memberof module:composer-runtime
 */
class Context {

    /**
     * Store a business network in the cache.
     * @param {string} businessNetworkHash The hash of the business network definition.
     * @param {BusinessNetworkDefinition} businessNetworkDefinition The business network definition.
     */
    static cacheBusinessNetwork(businessNetworkHash, businessNetworkDefinition) {
        const method = 'cacheBusinessNetwork';
        LOG.entry(method, businessNetworkHash, businessNetworkDefinition);
        businessNetworkCache.set(businessNetworkHash, businessNetworkDefinition);
        LOG.exit(method);
    }

    /**
     * Constructor.
     * @param {Engine} engine The chaincode engine that owns this context.
     */
    constructor(engine) {
        this.engine = engine;
        this.businessNetworkDefinition = null;
        this.registryManager = null;
        this.resolver = null;
        this.api = null;
        this.queryExecutor = null;
        this.identityManager = null;
        this.participant = null;
        this.transaction = null;
        this.transactionExecutors = [];
        this.accessController = null;
    }

    /**
     * Initialize the context for use.
     * @param {boolean} [reinitialize] Set to true if being reinitialized as a result
     * of an upgrade to the business network, falsey value if not.
     * @return {Promise} A promise that will be resolved when complete, or rejected
     * with an error.
     */
    initialize(reinitialize) {
        const method = 'initialize';
        LOG.entry(method, !!reinitialize);

        // Load the business network from the archive.
        LOG.debug(method, 'Getting $sysdata collection');
        return this.getDataService().getCollection('$sysdata')
            .then((collection) => {

                // check if the network has been undeployed first. if is has throw exception.
                if (collection.undeployed){
                    throw new Error('Network has already been undeployed');
                }

                LOG.debug(method, 'Getting business network archive from the $sysdata collection');
                return collection.get('businessnetwork');
            })
            .then((object) => {
                LOG.debug(method, 'Looking in cache for business network', object.hash);
                let businessNetworkDefinition = businessNetworkCache.get(object.hash);
                if (businessNetworkDefinition) {
                    LOG.debug(method, 'Business network is in cache');
                    return businessNetworkDefinition;
                }
                LOG.debug(method, 'Business network is not in cache, loading');
                let businessNetworkArchive = Buffer.from(object.data, 'base64');
                return BusinessNetworkDefinition.fromArchive(businessNetworkArchive)
                    .then((businessNetworkDefinition) => {
                        Context.cacheBusinessNetwork(object.hash, businessNetworkDefinition);
                        return businessNetworkDefinition;
                    });
            })
            .then((businessNetworkDefinition) => {
                LOG.debug(method, 'Loaded business network archive');
                this.businessNetworkDefinition = businessNetworkDefinition;
                let currentUserID = this.getIdentityService().getCurrentUserID();
                LOG.debug(method, 'Got current user ID', currentUserID);
                if (currentUserID) {
                    return this.getIdentityManager().getParticipant(currentUserID)
                        .then((participant) => {
                            LOG.debug(method, 'Found current participant', participant.getFullyQualifiedIdentifier());
                            if (!reinitialize) {
                                this.setParticipant(participant);
                            } else {
                                LOG.debug(method, 'Not setting current participant as we are reinitializing');
                            }
                        })
                        .catch((error) => {
                            LOG.error(method, 'Could not find current participant', error);
                            throw new Error(`Could not determine the participant for identity '${currentUserID}'. The identity may be invalid or may have been revoked.`);
                        });
                } else {
                    // TODO: this is temporary whilst we migrate to requiring all
                    // users to have identities that are mapped to participants.
                    LOG.debug(method, 'Could not determine current user ID');
                }
            })
            .then(() => {
                LOG.debug(method, 'Installing default JavaScript transaction executor');
                this.addTransactionExecutor(new JSTransactionExecutor());
            })
            .then(() => {
                LOG.exit(method);
            });

    }

    /**
     * Get the data service provided by the chaincode container.
     * @abstract
     * @return {DataService} The data service provided by the chaincode container.
     */
    getDataService() {
        throw new Error('abstract function called');
    }

    /**
     * Get the identity service provided by the chaincode container.
     * @abstract
     * @return {IdentityService} The identity service provided by the chaincode container.
     */
    getIdentityService() {
        throw new Error('abstract function called');
    }

    /**
     * Get the model manager.
     * @return {ModelManager} The model manager.
     */
    getModelManager() {
        if (!this.businessNetworkDefinition) {
            throw new Error('must call initialize before calling this function');
        }
        return this.businessNetworkDefinition.getModelManager();
    }

    /**
     * Get the script manager.
     * @return {ScriptManager} The script manager.
     */
    getScriptManager() {
        if (!this.businessNetworkDefinition) {
            throw new Error('must call initialize before calling this function');
        }
        return this.businessNetworkDefinition.getScriptManager();
    }

    /**
     * Get the ACL manager.
     * @return {AclManager} The ACL manager.
     */
    getAclManager() {
        if (!this.businessNetworkDefinition) {
            throw new Error('must call initialize before calling this function');
        }
        return this.businessNetworkDefinition.getAclManager();
    }

    /**
     * Get the factory.
     * @return {Factory} The factory.
     */
    getFactory() {
        if (!this.businessNetworkDefinition) {
            throw new Error('must call initialize before calling this function');
        }
        return this.businessNetworkDefinition.getFactory();
    }

    /**
     * Get the serializer.
     * @return {Serializer} The serializer.
     */
    getSerializer() {
        if (!this.businessNetworkDefinition) {
            throw new Error('must call initialize before calling this function');
        }
        return this.businessNetworkDefinition.getSerializer();
    }

    /**
     * Get the introspector.
     * @return {Introspector} The serializer.
     */
    getIntrospector() {
        if (!this.businessNetworkDefinition) {
            throw new Error('must call initialize before calling this function');
        }
        return this.businessNetworkDefinition.getIntrospector();
    }

    /**
     * Get the registry manager.
     * @return {RegistryManager} The registry manager.
     */
    getRegistryManager() {
        if (!this.registryManager) {
            this.registryManager = new RegistryManager(this.getDataService(), this.getIntrospector(), this.getSerializer(), this.getAccessController());
        }
        return this.registryManager;
    }

    /**
     * Get the resolver.
     * @return {Resolver} The resolver.
     */
    getResolver() {
        if (!this.resolver) {
            this.resolver = new Resolver(this.getIntrospector(), this.getRegistryManager());
        }
        return this.resolver;
    }

    /**
     * Get the API.
     * @return {Api} The API.
     */
    getApi() {
        if (!this.api) {
            this.api = new Api(this.getFactory(), this.getParticipant(), this.getRegistryManager());
        }
        return this.api;
    }

    /**
     * Get the query executor.
     * @return {QueryExecutor} The query executor.
     */
    getQueryExecutor() {
        if (!this.queryExecutor) {
            this.queryExecutor = new QueryExecutor(this.getResolver());
        }
        return this.queryExecutor;
    }

    /**
     * Get the identity manager.
     * @return {IdentityManager} The identity manager.
     */
    getIdentityManager() {
        if (!this.identityManager) {
            this.identityManager = new IdentityManager(this.getDataService(), this.getRegistryManager());
        }
        return this.identityManager;
    }

    /**
     * Get the current participant.
     * @return {Resource} the current participant.
     */
    getParticipant() {
        return this.participant;
    }

    /**
     * Set the current participant.
     * @param {Resource} participant the current participant.
     */
    setParticipant(participant) {
        if (this.participant) {
            throw new Error('A current participant has already been specified');
        }
        this.participant = participant;
        this.getAccessController().setParticipant(participant);
    }

    /**
     * Get the current transaction.
     * @return {Resource} the current transaction.
     */
    getTransaction() {
        return this.transaction;
    }

    /**
     * Set the current transaction.
     * @param {Resource} transaction the current transaction.
     */
    setTransaction(transaction) {
        if (this.transaction) {
            throw new Error('A current transaction has already been specified');
        }
        this.transaction = transaction;
        this.transactionLogger = new TransactionLogger(this.transaction, this.getRegistryManager(), this.getSerializer());
    }

    /**
     * Add a transaction executor.
     * @param {TransactionExecutor} transactionExecutor The transaction executor.
     */
    addTransactionExecutor(transactionExecutor) {
        const method = 'addTransactionExecutor';
        LOG.entry(method, transactionExecutor);
        let replaced = this.transactionExecutors.some((existingTransactionExecutor, index) => {
            if (transactionExecutor.getType() === existingTransactionExecutor.getType()) {
                LOG.debug(method, 'Found existing executor for type, replacing', transactionExecutor.getType());
                this.transactionExecutors[index] = transactionExecutor;
                return true;
            } else {
                return false;
            }
        });
        if (!replaced) {
            LOG.debug(method, 'Did not replace executor, adding to end of list', transactionExecutor.getType());
            this.transactionExecutors.push(transactionExecutor);
        }
        LOG.exit(method);
    }

    /**
     * Get the list of transaction executors.
     * @return {TransactionExecutor[]} The list of transaction executors.
     */
    getTransactionExecutors() {
        return this.transactionExecutors;
    }

    /**
     * Get the access controller.
     * @return {AccessController} The access controller.
     */
    getAccessController() {
        if (!this.accessController) {
            this.accessController = new AccessController(this.getAclManager());
        }
        return this.accessController;
    }

    /**
     * Stop serialization of this object.
     * @return {Object} An empty object.
     */
    toJSON() {
        return {};
    }

}

module.exports = Context;