<commit_before>
module.exports = {
	routes: {
		HelloWorld: {
			path: ['/'],
			method: 'get',
			page: './pages/hello-world',
		},
	},
};
<commit_msg>Fix 599: use path.join instead of string concatenation (#601)<commit_after>const path = require('path');

module.exports = {
	routes: {
		HelloWorld: {
			path: ['/'],
			method: 'get',
			page: path.join('.', 'pages', 'hello-world'),
		},
	},
};
<|endoftext|>


<commit_before>'use strict';

var
  Promise = require('bluebird'),
  Deque = require('denque'),
  MemoryStorageController = require('./memoryStorageController'),
  WriteController = require('./writeController'),
  ReadController = require('./readController'),
  SubscribeController = require('./subscribeController'),
  BulkController = require('./bulkController'),
  AdminController = require('./adminController'),
  SecurityController = require('./securityController'),
  AuthController = require('./authController'),
  BadRequestError = require('kuzzle-common-objects').errors.BadRequestError,
  UnauthorizedError = require('kuzzle-common-objects').errors.UnauthorizedError,
  ForbiddenError = require('kuzzle-common-objects').errors.ForbiddenError,
  ServiceUnavailableError = require('kuzzle-common-objects').errors.ServiceUnavailableError,
  Request = require('kuzzle-common-objects').Request;

const MIN_TIME_BEFORE_DUMP_PREV_ERROR = 60000;

class CacheItem {
  constructor(request, callback) {
    this.request = request;
    this.callback = callback;
  }
}

/**
 * @property {object} controllers
 * @param {Kuzzle} kuzzle
 */
function FunnelController (kuzzle) {
  this.overloaded = false;
  this.concurrentRequests = 0;
  this.controllers = {};
  this.requestsCache = new Deque();
  this.cachedItems = 0;
  this.lastOverloadTime = 0;
  this.overloadWarned = false;
  this.lastWarningTime = 0;

  this.lastDumpedErrors = {};

  // used only for core-dump analysis
  this.pendingRequests = {};

  // used only for core-dump analysis
  this.requestHistory = new Deque();
  this.historized = 0;

  this.init = function funnelInit () {
    this.controllers.admin = new AdminController(kuzzle);
    this.controllers.auth = new AuthController(kuzzle);
    this.controllers.bulk = new BulkController(kuzzle);
    this.controllers.memoryStorage = this.controllers.ms = new MemoryStorageController(kuzzle);
    this.controllers.read = new ReadController(kuzzle);
    this.controllers.security = new SecurityController(kuzzle);
    this.controllers.subscribe = new SubscribeController(kuzzle);
    this.controllers.write = new WriteController(kuzzle);

    kuzzle.pluginsManager.injectControllers();
  };

  /**
   * Execute the right controller/action
   *
   * @param {Request} request
   * @param {Function} callback
   */
  this.execute = function funnelExecute (request, callback) {
    if (this.overloaded) {
      let now = Date.now();

      if ((this.cachedItems > kuzzle.config.server.warningRetainedRequestsLimit) && (this.lastWarningTime === 0 || this.lastWarningTime < now - 500)) {
        let overloadPercentage = Math.round(10000 * this.cachedItems / kuzzle.config.server.maxRetainedRequests) / 100;
        kuzzle.pluginsManager.trigger('server:overload', overloadPercentage);
        kuzzle.pluginsManager.trigger('log:warn', '[!WARNING!] Kuzzle overloaded: ' + overloadPercentage + '%. Delaying requests...');

        this.overloadWarned = true;
        this.lastWarningTime = now;
      }
    }

    // execute the request immediately if kuzzle is not overloaded
    if (this.concurrentRequests < kuzzle.config.server.maxConcurrentRequests) {
      this.concurrentRequests++;

      this.pendingRequests[request.id] = request;

      processRequest(kuzzle, this, request)
        .then(processResult => callback(null, processResult))
        .catch(err => {
          kuzzle.pluginsManager.trigger('log:error', err);

          request.setError(err);
          callback(err, request);

          this.handleErrorDump(err);

          return null;
        })
        .finally(() => {
          this.concurrentRequests--;
          delete this.pendingRequests[request.id];
        });

      return null;
    }

    /*
      If kuzzle is overloaded, check the requests cache.
      There are two possibilities:
        1- the cache limit has not been reached. A warning is emitted, and the
           request is cached and will be played as soon as the maxConcurrentRequests
           property allows it

        2- the number of cached requests is equal to the maxRetainedRequests property.
           The request is then discarded and an error is returned to the sender
     */
    if (!this.overloaded) {
      this.overloaded = true;
      playCachedRequests(kuzzle, this);
    }

    if (this.cachedItems >= kuzzle.config.server.maxRetainedRequests) {
      const error = new ServiceUnavailableError('Request discarded: Kuzzle Server is temporarily overloaded');

      kuzzle.pluginsManager.trigger('log:error', error);
      request.setError(error);
      callback(error, request);
      return null;
    }

    this.cachedItems++;
    this.requestsCache.push(new CacheItem(request, callback));
  };

  this.handleErrorDump = function funnelHandleErrorDump (err) {
    if (kuzzle.config.dump.enabled && kuzzle.config.dump.handledErrors.enabled) {
      setImmediate(() => {
        var
          lastSeen = Date.now(),
          request,
          errorType,
          errorMessage;

        errorType = typeof err === 'object' && err.name ? err.name : typeof err;

        if (kuzzle.config.dump.handledErrors.whitelist.indexOf(errorType) > -1) {
          request = new Request({
            controller: 'actions',
            action: 'dump',
            body: {}
          });

          // simplify error message to use it in folder dump name
          errorMessage = err.message;
          if (errorMessage.indexOf('\n') > -1) {
            errorMessage = errorMessage.split('\n')[0];
          }
          errorMessage = errorMessage.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, '-').replace(/[-]+/g, '-').split('-');
          errorMessage = errorMessage.filter(value => value !== '').join('-');

          request.data.body.suffix = 'handled-'.concat(errorType.toLowerCase(), '-', errorMessage);

          if (!this.lastDumpedErrors[request.data.body.suffix] || this.lastDumpedErrors[request.data.body.suffix] < lastSeen - MIN_TIME_BEFORE_DUMP_PREV_ERROR) {
            kuzzle.cliController.actions.dump(request);
            this.lastDumpedErrors[request.data.body.suffix] = lastSeen;
          }
        }
      });
    }
  };
}

/**
 * Execute the request immediately
 * @param {Kuzzle} kuzzle
 * @param {FunnelController} funnel
 * @param {Request} request
 */
function processRequest(kuzzle, funnel, request) {
  var controllers = funnel.controllers;

  kuzzle.statistics.startRequest(request);

  if (funnel.historized > kuzzle.config.server.maxRequestHistorySize) {
    funnel.requestHistory.shift();
  }
  else {
    funnel.historized++;
  }

  funnel.requestHistory.push(request);

  if (
    !controllers[request.input.controller]
    || !controllers[request.input.controller][request.input.action]
    || typeof controllers[request.input.controller][request.input.action] !== 'function'
  ) {
    return Promise.reject(new BadRequestError(`No corresponding action ${request.input.action} in controller ${request.input.controller}`));
  }

  return kuzzle.repositories.token.verifyToken(request.input.jwt)
    .then(userToken => {
      request.context.token = userToken;
      return kuzzle.repositories.user.load(request.context.token.userId);
    })
    .then(user => user.isActionAllowed(request, kuzzle))
    .then(isAllowed => {
      var
        modifiedRequest,
        input = request.input,
        beforeEvent = getEventName(input.controller, 'before', input.action),
        afterEvent = getEventName(input.controller, 'after', input.action);

      if (!isAllowed) {
        // anonymous user => we throw a 401 (Unauthorised) error
        if (request.context.token.userId === -1) {
          return Promise.reject(new UnauthorizedError(
            `Unauthorized action [${input.index}/${input.collection}/${input.controller}/${input.action}] for anonymous user`)
          );
        }
        // logged-in user with insufficient permissions => we throw a 403 (Forbidden) error
        return Promise.reject(new ForbiddenError(
          `Forbidden action [${input.index}/${input.collection}/${input.controller}/${input.action}] for user ${request.context.token.userId}`)
        );
      }

      return kuzzle.pluginsManager.trigger(beforeEvent, request)
        .then(newRequest => {
          modifiedRequest = newRequest;

          return controllers[request.input.controller][request.input.action](newRequest);
        })
        .then(responseData => {
          modifiedRequest.setResult(responseData, request.status === 102 ? 200 : request.status);

          return kuzzle.pluginsManager.trigger(afterEvent, modifiedRequest);
        });
    })
    .then(newRequest => {
      kuzzle.statistics.completedRequest(request);

      return newRequest;
    })
    .catch(error => {
      kuzzle.statistics.failedRequest(request);
      return Promise.reject(error);
    });
}

/**
 * Background task. Checks if there are any requests in cache, and replay them
 * if Kuzzle is not overloaded anymore,
 *
 * @param kuzzle
 * @param {FunnelController} funnel
 */
function playCachedRequests(kuzzle, funnel) {
  // If there is room to play bufferized requests, do it now. If not, retry later
  let quantityToInject = funnel.cachedItems > 0 ?
    Math.min(funnel.cachedItems, kuzzle.config.server.maxConcurrentRequests - funnel.concurrentRequests)
    : 0;

  for(let i = 0; i < quantityToInject; i++) {
    let cachedItem = funnel.requestsCache.shift();
    funnel.cachedItems--;
    funnel.execute(cachedItem.request, cachedItem.callback);
  }

  if (funnel.cachedItems > 0) {
    setTimeout(() => playCachedRequests(kuzzle, funnel), 0);
  } else {
    let now = Date.now();
    // No request remaining in cache => stop the background task and return to normal behavior
    funnel.overloaded = false;

    if (funnel.overloadWarned && (funnel.lastOverloadTime === 0 || funnel.lastOverloadTime < now - 500)) {
      funnel.overloadWarned = false;
      kuzzle.pluginsManager.trigger('log:info', 'End of overloaded state. Resuming normal activity.');
      funnel.lastOverloadTime = now;
    }
  }
}

function getEventName(controller, prefix, action) {
  return controller + ':' + prefix + capitalizeFirstLetter(action);
}

/**
 * @param {string} string
 * @returns {string}
 */
function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

module.exports = FunnelController;
<commit_msg>bugfix: message delayed until next overload mode<commit_after>'use strict';

var
  Promise = require('bluebird'),
  Deque = require('denque'),
  MemoryStorageController = require('./memoryStorageController'),
  WriteController = require('./writeController'),
  ReadController = require('./readController'),
  SubscribeController = require('./subscribeController'),
  BulkController = require('./bulkController'),
  AdminController = require('./adminController'),
  SecurityController = require('./securityController'),
  AuthController = require('./authController'),
  BadRequestError = require('kuzzle-common-objects').errors.BadRequestError,
  UnauthorizedError = require('kuzzle-common-objects').errors.UnauthorizedError,
  ForbiddenError = require('kuzzle-common-objects').errors.ForbiddenError,
  ServiceUnavailableError = require('kuzzle-common-objects').errors.ServiceUnavailableError,
  Request = require('kuzzle-common-objects').Request;

const MIN_TIME_BEFORE_DUMP_PREV_ERROR = 60000;

class CacheItem {
  constructor(request, callback) {
    this.request = request;
    this.callback = callback;
  }
}

/**
 * @property {object} controllers
 * @param {Kuzzle} kuzzle
 */
function FunnelController (kuzzle) {
  this.overloaded = false;
  this.concurrentRequests = 0;
  this.controllers = {};
  this.requestsCache = new Deque();
  this.cachedItems = 0;
  this.lastOverloadTime = 0;
  this.overloadWarned = false;
  this.lastWarningTime = 0;

  this.lastDumpedErrors = {};

  // used only for core-dump analysis
  this.pendingRequests = {};

  // used only for core-dump analysis
  this.requestHistory = new Deque();
  this.historized = 0;

  this.init = function funnelInit () {
    this.controllers.admin = new AdminController(kuzzle);
    this.controllers.auth = new AuthController(kuzzle);
    this.controllers.bulk = new BulkController(kuzzle);
    this.controllers.memoryStorage = this.controllers.ms = new MemoryStorageController(kuzzle);
    this.controllers.read = new ReadController(kuzzle);
    this.controllers.security = new SecurityController(kuzzle);
    this.controllers.subscribe = new SubscribeController(kuzzle);
    this.controllers.write = new WriteController(kuzzle);

    kuzzle.pluginsManager.injectControllers();
  };

  /**
   * Execute the right controller/action
   *
   * @param {Request} request
   * @param {Function} callback
   */
  this.execute = function funnelExecute (request, callback) {
    if (this.overloaded) {
      let now = Date.now();

      if ((this.cachedItems > kuzzle.config.server.warningRetainedRequestsLimit) && (this.lastWarningTime === 0 || this.lastWarningTime < now - 500)) {
        let overloadPercentage = Math.round(10000 * this.cachedItems / kuzzle.config.server.maxRetainedRequests) / 100;
        kuzzle.pluginsManager.trigger('server:overload', overloadPercentage);
        kuzzle.pluginsManager.trigger('log:warn', '[!WARNING!] Kuzzle overloaded: ' + overloadPercentage + '%. Delaying requests...');

        this.overloadWarned = true;
        this.lastWarningTime = now;
      }
    }

    // execute the request immediately if kuzzle is not overloaded
    if (this.concurrentRequests < kuzzle.config.server.maxConcurrentRequests) {
      this.concurrentRequests++;

      this.pendingRequests[request.id] = request;

      processRequest(kuzzle, this, request)
        .then(processResult => callback(null, processResult))
        .catch(err => {
          kuzzle.pluginsManager.trigger('log:error', err);

          request.setError(err);
          callback(err, request);

          this.handleErrorDump(err);

          return null;
        })
        .finally(() => {
          this.concurrentRequests--;
          delete this.pendingRequests[request.id];
        });

      return null;
    }

    if (this.cachedItems >= kuzzle.config.server.maxRetainedRequests) {
      const error = new ServiceUnavailableError('Request discarded: Kuzzle Server is temporarily overloaded');

      kuzzle.pluginsManager.trigger('log:error', error);
      request.setError(error);
      callback(error, request);
      return null;
    }

    this.cachedItems++;
    this.requestsCache.push(new CacheItem(request, callback));

    /*
     If kuzzle is overloaded, check the requests cache.
     There are two possibilities:
     1- the cache limit has not been reached. A warning is emitted, and the
     request is cached and will be played as soon as the maxConcurrentRequests
     property allows it

     2- the number of cached requests is equal to the maxRetainedRequests property.
     The request is then discarded and an error is returned to the sender
     */
    if (!this.overloaded) {
      this.overloaded = true;

      /*
       /!\ Invoking this function here with setTimeout() leads to V8 deoptimizing
       the entire funnelExecute method (as of node.js 6.9.1),
       because of an "out of bounds" heuristic error (caused by node's
       setTimeout code written in JS? this needs further investigations)

       We get better performances executing this function once for nothing each
       time we start overload mode by keeping this method optimized by crankshaft.
       */
      playCachedRequests(kuzzle, this);
    }
  };

  this.handleErrorDump = function funnelHandleErrorDump (err) {
    if (kuzzle.config.dump.enabled && kuzzle.config.dump.handledErrors.enabled) {
      setImmediate(() => {
        var
          lastSeen = Date.now(),
          request,
          errorType,
          errorMessage;

        errorType = typeof err === 'object' && err.name ? err.name : typeof err;

        if (kuzzle.config.dump.handledErrors.whitelist.indexOf(errorType) > -1) {
          request = new Request({
            controller: 'actions',
            action: 'dump',
            body: {}
          });

          // simplify error message to use it in folder dump name
          errorMessage = err.message;
          if (errorMessage.indexOf('\n') > -1) {
            errorMessage = errorMessage.split('\n')[0];
          }
          errorMessage = errorMessage.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, '-').replace(/[-]+/g, '-').split('-');
          errorMessage = errorMessage.filter(value => value !== '').join('-');

          request.data.body.suffix = 'handled-'.concat(errorType.toLowerCase(), '-', errorMessage);

          if (!this.lastDumpedErrors[request.data.body.suffix] || this.lastDumpedErrors[request.data.body.suffix] < lastSeen - MIN_TIME_BEFORE_DUMP_PREV_ERROR) {
            kuzzle.cliController.actions.dump(request);
            this.lastDumpedErrors[request.data.body.suffix] = lastSeen;
          }
        }
      });
    }
  };
}

/**
 * Execute the request immediately
 * @param {Kuzzle} kuzzle
 * @param {FunnelController} funnel
 * @param {Request} request
 */
function processRequest(kuzzle, funnel, request) {
  var controllers = funnel.controllers;

  kuzzle.statistics.startRequest(request);

  if (funnel.historized > kuzzle.config.server.maxRequestHistorySize) {
    funnel.requestHistory.shift();
  }
  else {
    funnel.historized++;
  }

  funnel.requestHistory.push(request);

  if (
    !controllers[request.input.controller]
    || !controllers[request.input.controller][request.input.action]
    || typeof controllers[request.input.controller][request.input.action] !== 'function'
  ) {
    return Promise.reject(new BadRequestError(`No corresponding action ${request.input.action} in controller ${request.input.controller}`));
  }

  return kuzzle.repositories.token.verifyToken(request.input.jwt)
    .then(userToken => {
      request.context.token = userToken;
      return kuzzle.repositories.user.load(request.context.token.userId);
    })
    .then(user => user.isActionAllowed(request, kuzzle))
    .then(isAllowed => {
      var
        modifiedRequest,
        input = request.input,
        beforeEvent = getEventName(input.controller, 'before', input.action),
        afterEvent = getEventName(input.controller, 'after', input.action);

      if (!isAllowed) {
        // anonymous user => we throw a 401 (Unauthorised) error
        if (request.context.token.userId === -1) {
          return Promise.reject(new UnauthorizedError(
            `Unauthorized action [${input.index}/${input.collection}/${input.controller}/${input.action}] for anonymous user`)
          );
        }
        // logged-in user with insufficient permissions => we throw a 403 (Forbidden) error
        return Promise.reject(new ForbiddenError(
          `Forbidden action [${input.index}/${input.collection}/${input.controller}/${input.action}] for user ${request.context.token.userId}`)
        );
      }

      return kuzzle.pluginsManager.trigger(beforeEvent, request)
        .then(newRequest => {
          modifiedRequest = newRequest;

          return controllers[request.input.controller][request.input.action](newRequest);
        })
        .then(responseData => {
          modifiedRequest.setResult(responseData, request.status === 102 ? 200 : request.status);

          return kuzzle.pluginsManager.trigger(afterEvent, modifiedRequest);
        });
    })
    .then(newRequest => {
      kuzzle.statistics.completedRequest(request);

      return newRequest;
    })
    .catch(error => {
      kuzzle.statistics.failedRequest(request);
      return Promise.reject(error);
    });
}

/**
 * Background task. Checks if there are any requests in cache, and replay them
 * if Kuzzle is not overloaded anymore,
 *
 * @param kuzzle
 * @param {FunnelController} funnel
 */
function playCachedRequests(kuzzle, funnel) {
  // If there is room to play bufferized requests, do it now. If not, retry later
  let quantityToInject = funnel.cachedItems > 0 ?
    Math.min(funnel.cachedItems, kuzzle.config.server.maxConcurrentRequests - funnel.concurrentRequests)
    : 0;

  for(let i = 0; i < quantityToInject; i++) {
    let cachedItem = funnel.requestsCache.shift();
    funnel.execute(cachedItem.request, cachedItem.callback);
  }

  funnel.cachedItems -= quantityToInject;

  if (funnel.cachedItems > 0) {
    setTimeout(() => playCachedRequests(kuzzle, funnel), 0);
  } else {
    let now = Date.now();
    // No request remaining in cache => stop the background task and return to normal behavior
    funnel.overloaded = false;

    if (funnel.overloadWarned && (funnel.lastOverloadTime === 0 || funnel.lastOverloadTime < now - 500)) {
      funnel.overloadWarned = false;
      kuzzle.pluginsManager.trigger('log:info', 'End of overloaded state. Resuming normal activity.');
      funnel.lastOverloadTime = now;
    }
  }
}

function getEventName(controller, prefix, action) {
  return controller + ':' + prefix + capitalizeFirstLetter(action);
}

/**
 * @param {string} string
 * @returns {string}
 */
function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

module.exports = FunnelController;
<|endoftext|>


<commit_before>var
  _ = require('lodash'),
  q = require('q'),
  stringify = require('json-stable-stringify'),
  Router = require('router'),
  bodyParser = require('body-parser'),
  finalhandler = require('finalhandler'),
  RequestObject = require('../core/models/requestObject'),
  ResponseObject = require('../core/models/responseObject'),
  PluginImplementationError = require('../core/errors/pluginImplementationError'),
  BadRequestError = require('../core/errors/badRequestError');

module.exports = function RouterController (kuzzle) {
  this.router = null;
  this.pluginRouter = null;
  this.routename = 'kuzzle';
  this.kuzzle = kuzzle;
  this.connections = {};

  /**
   * Declares a new connection on a given protocol. Called by protocol plugins.
   * Returns a context object to be used with router.execute() and router.removeConnection()
   *
   * @param {string} protocol - protocol name
   * @param {string} connectionId - unique connection identifier
   * @return {promise} connection context, to be used with other router functions
   */
  this.newConnection = function (protocol, connectionId) {
    var error;

    if (!connectionId || !protocol || typeof connectionId !== 'string' || typeof protocol !== 'string') {
      error = new PluginImplementationError('Rejected new connection declaration: invalid arguments');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(error);
    }

    if (!this.connections[connectionId]) {
      this.connections[connectionId] = {
        connection: {type: protocol, id: connectionId},
        user: null
      };
    }

    kuzzle.statistics.newConnection(this.connections[connectionId]);

    return q(this.connections[connectionId]);
  };

  /**
   * Called by protocol plugins: forward a received request to Kuzzle.
   * Return a promise resolved or rejected with the corresponding ResponseObject
   *
   * A note about the JWT headers: if this value is falsey, if no "authorization" field is found, if the token is not
   * properly formatted or if the token itself is invalid, then the corresponding user will automatically
   * be set to "anonymous".
   *
   * @param {Object} jwtHeaders - Optional JWT headers
   * @param {Object} requestObject - the request to execute
   * @param {String} context - connection context, obtained using the newConnection() method
   * @return {Promise} ResponseObject
   */
  this.execute = function (jwtHeaders, requestObject, context) {
    var error;

    if (!requestObject) {
      error = new PluginImplementationError('Request execution error: no provided request');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(new ResponseObject({}, error));
    }

    if (!context || !context.connection) {
      error = new PluginImplementationError('Unable to execute request: ' + requestObject +
        '\nReason: invalid context. Use context.getRouter().newConnection() to get a valid context.');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(new ResponseObject({}, error));
    }

    if (!context.connection.id || !this.connections[context.connection.id]) {
      error = new PluginImplementationError('Unable to execute request: unknown context. ' +
        'Has context.getRouter().newConnection() been called before executing requests?');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(new ResponseObject({}, error));
    }

    return kuzzle.repositories.user.loadFromToken(getBearerTokenFromHeaders(jwtHeaders))
      .then(user => {
        context.user = user;

        return kuzzle.funnel.execute(requestObject, context);
      })
      .then(response => {
        return response;
      })
      .catch(e => {
        kuzzle.pluginsManager.trigger('log:error', e);
        return q.reject(new ResponseObject({}, e));
      });
  };

  /**
   * Called by protocol plugins: removes a connection from the connection pool.
   * @param {object} context - connection context, obtained using the newConnection() method
   */
  this.removeConnection = function (context) {
    if (context.connection.id && this.connections[context.connection.id]) {
      delete this.connections[context.connection.id];
      kuzzle.hotelClerk.removeCustomerFromAllRooms(context.connection);
      kuzzle.statistics.dropConnection(context.connection);
    }
    else {
      kuzzle.pluginsManager.trigger('log:error', new PluginImplementationError('Unable to remove connection: ' +
        context + '.\nReason: unknown context'));
    }
  };

  /**
   * Initializes the HTTP routes for the Kuzzle REST API.
   */
  this.initRouterHttp = function () {
    var
      apiBase = new Router(),
      api = new Router(),
      coverage,
      routes = [
        {verb: 'get', url: '/_getLastStats', controller: 'admin', action: 'getLastStats'},
        {verb: 'get', url: '/_getAllStats', controller: 'admin', action: 'getAllStats'},
        {verb: 'get', url: '/_now', controller: 'read', action: 'now'},
        {verb: 'get', url: '/_listIndexes', controller: 'read', action: 'listIndexes'},
        {verb: 'get', url: '/_listSubscriptions', controller: 'subscribe', action: 'list'},
        {verb: 'get', url: '/:index/_listCollections', controller: 'read', action: 'listCollections'},
        {verb: 'get', url: '/:index/_listCollections/:type', controller: 'read', action: 'listCollections'},
        {verb: 'get', url: '/:index/:collection/_mapping', controller: 'admin', action: 'getMapping'},
        {verb: 'get', url: '/:index/:collection/:id', controller: 'read', action: 'get'},

        {verb: 'post', url: '/_bulk', controller: 'bulk', action: 'import'},
        {verb: 'post', url: '/_getStats', controller: 'admin', action: 'getStats'},
        {verb: 'post', url: '/_login', controller: 'auth', action: 'login'},
        {verb: 'post', url: '/:index/_bulk', controller: 'bulk', action: 'import'},
        {verb: 'post', url: '/:index/:collection/_bulk', controller: 'bulk', action: 'import'},
        {verb: 'post', url: '/:index/:collection/_search', controller: 'read', action: 'search'},
        {verb: 'post', url: '/:index/:collection/_count', controller: 'read', action: 'count'},
        {verb: 'post', url: '/:index/:collection/_create', controller: 'write', action: 'create'},
        {verb: 'post', url: '/:index/:collection', controller: 'write', action: 'publish'},

        {verb: 'delete', url: '/_deleteIndexes', controller: 'admin', action: 'deleteIndexes'},
        {verb: 'delete', url: '/:index', controller: 'admin', action: 'deleteIndex'},
        {verb: 'delete', url: '/:index/:collection/_query', controller: 'write', action: 'deleteByQuery'},
        {verb: 'delete', url: '/:index/:collection/_truncate', controller: 'admin', action: 'truncateCollection'},
        {verb: 'delete', url: '/:index/:collection/:id', controller: 'write', action: 'delete'},
        {verb: 'delete', url: '/:index/:collection', controller: 'admin', action: 'deleteCollection'},

        {verb: 'put', url: '/_role/:id', controller: 'admin', action: 'putRole'},
        {verb: 'put', url: '/:index', controller: 'admin', action: 'createIndex'},
        {verb: 'put', url: '/:index/:collection', controller: 'write', action: 'createCollection'},
        {verb: 'put', url: '/:index/:collection/_mapping', controller: 'admin', action: 'putMapping'},
        {verb: 'put', url: '/:index/:collection/:id/_:action', controller: 'write'},
        {verb: 'put', url: '/:index/:collection/:id', controller: 'write', action: 'createOrUpdate'}
      ];
    routes = routes.concat(kuzzle.pluginsManager.routes);

    this.router = new Router();

    // create and mount a new router for the coverage API
    if (process.env.FEATURE_COVERAGE === '1') {
      coverage = require('istanbul-middleware');
      this.router.use('/coverage', coverage.createHandler({ resetOnGet: true }));
    }

    // create and mount a new router for our API
    this.router.use('/api', apiBase);
    this.router.use('/api/' + kuzzle.config.apiVersion, api);

    /*
     Registering the basic _serverInfo route
     This route is also used to get Kuzzle API Version, so it isn't registered under api/<version> but
     directly under api/
     */
    apiBase.get('/_serverInfo', (request, response) => {
      executeFromRest.call(kuzzle, {controller: 'read', action: 'serverInfo'}, request, response);
    });

    // create and mount a new router for plugins
    this.pluginRouter = new Router();
    api.use('/_plugin', this.pluginRouter);

    // add a body parsing middleware to our API
    api.use(bodyParser.json());

    // Simple hello world to let know to the user that kuzzle is running
    api.get('/', (request, response) => {
      response.writeHead('Access-Control-Allow-Origin', '*');
      response.writeHead('Access-Control-Allow-Headers', 'X-Requested-With');
      response.writeHead(200, {'Content-Type': 'application/json'});
      response.end(stringify({status: 200, error: null, result: 'Hello from Kuzzle :)'}));
    });

    // Register API routes
    routes.forEach(route => {
      api[route.verb](route.url, function (request, response) {
        var params = {
          controller: route.controller
        };

        if (route.action) {
          params.action = route.action;
        }

        executeFromRest.call(kuzzle, params, request, response);
      });
    });
  };

  /**
   * Forward incoming REST requests to the HTTP routes created by the
   * initRouterHttp function.
   *
   * @param request transmitted through the REST API
   * @param response is the HTTP connection handler
   */
  this.routeHttp = function (request, response) {
    kuzzle.pluginsManager.trigger('log:silly', 'Handle HTTP request');

    this.router(request, response, finalhandler(request, response));
  };

  /**
   * Handles requests coming from MQ protocols: AMQP, MQTT & STOMP
   *
   */
  this.routeMQListener = function () {
    kuzzle.services.list.mqBroker.listenExchange(this.routename, function handleMQMessage(msg) {
      var
        context = {
          connection: null,
          user: null
        },
        data,
        requestObject,
        rawContent;

      if (!(msg.content instanceof Buffer)) {
        rawContent = msg.content.toString();
      }
      else {
        rawContent = (new Buffer(msg.content)).toString();
      }

      try {
        data = JSON.parse(rawContent);
      }
      catch (e) {
        kuzzle.pluginsManager.trigger('log:error', {message: 'Parse error', error: e});
        return false;
      }

      kuzzle.pluginsManager.trigger('log:silly', 'Handle MQ input' + msg.fields.routingKey);

      // For MQTT messages, we do not have a replyTo header like with AMQP or STOMP
      // => MQTT client has to send its mqtt client id and subscribe to the topic exchange mqtt.<clientId>
      //    to get feedback from Kuzzle.
      if (msg.properties && msg.properties.replyTo) {
        context.connection = {type: 'amq', id: data.clientId};
      }
      else {
        context.connection = {type: 'mqtt', id: data.clientId};
      }

      requestObject = new RequestObject(data, {}, 'mq');

      kuzzle.repositories.user.loadFromToken(getBearerTokenFromHeaders(data.headers))
        .then(function (user) {
          context.user = user;
          return kuzzle.funnel.execute(requestObject, context);
        })
        .then(function (responseObject) {
          if (context.connection.type === 'amq') {
            kuzzle.services.list.mqBroker.replyTo(msg.properties.replyTo, responseObject.toJson());
          }
          else {
            kuzzle.services.list.mqBroker.addExchange('mqtt.' + context.connection.id, responseObject.toJson());
          }
        })
        .catch(function (error) {
          var errorObject = new ResponseObject({}, error);
          kuzzle.pluginsManager.trigger('log:error', error);
          if (context.connection.type === 'amq') {
            kuzzle.services.list.mqBroker.replyTo(msg.properties.replyTo, errorObject.toJson());
          }
          else {
            kuzzle.services.list.mqBroker.addExchange('mqtt.' + context.connection.id, errorObject.toJson());
          }
        });
    });
  };

};

/**
 * Transmit HTTP requests to the funnel controller and forward its response back to
 * the client
 *
 * @param params contains the request metadata
 * @param request is the original request from the client
 * @param response is the HTTP connection handler
 */
function executeFromRest(params, request, response) {
  var
    requestObject,
    errorObject,
    data,
    context = {
      connection: {type: 'rest'},
      user: null
    };

  if (!params.controller) {
    errorObject = new ResponseObject({}, new BadRequestError('The "controller" argument is missing'));
    response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
    response.end(stringify(errorObject.toJson()));
    return false;
  }

  if (request.headers['content-type'] && request.headers['content-type'] !== 'application/json') {
    errorObject = new ResponseObject({}, new BadRequestError('Invalid request content-type. Expected "application/json", got: "' + request.headers['content-type'] +'"'));
    response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
    response.end(stringify(errorObject.toJson()));
    return false;
  }

  data = {
    controller: params.controller,
    action: params.action || request.params.action,
    collection: request.params.collection
  };

  if (request.params.action) {
    delete request.params.action;
  }
  if (request.params.collection) {
    delete request.params.collection;
  }
  if (request.params.id) {
    data._id = request.params.id;
    delete request.params.id;
  }

  if (request.params.index) {
    data.index = request.params.index;
    delete request.params.index;
  }

  _.forEach(request.params, function (value, param) {
    request.body[param] = value;
  });

  requestObject = new RequestObject(data, request.body, 'rest');

  response.setHeader('Access-Control-Allow-Origin', '*');
  response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With');

  this.repositories.user.loadFromToken(getBearerTokenFromHeaders(request.headers))
    .then(function (user) {
      context.user = user;

      return this.funnel.execute(requestObject, context);
    }.bind(this))
    .then(function (responseObject) {
      response.writeHead(responseObject.status, {'Content-Type': 'application/json'});
      response.end(stringify(responseObject.toJson()));
    })
    .catch(function (error) {
      errorObject = new ResponseObject(requestObject, error);
      this.emit(requestObject.controller + ':websocket:funnel:reject', error);
      this.pluginsManager.trigger('log:error', error);
      response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
      response.end(stringify(errorObject.toJson()));
    }.bind(this));
}

/**
 * Extract the Bearer token from the given headers
 * @param {Object} headers
 * @returns {*}
 */
function getBearerTokenFromHeaders (headers) {
  var
    r;

  if (headers !== undefined && headers.authorization !== undefined) {
    r = /^Bearer (.+)$/.exec(headers.authorization);
    if (r !== null && r[1].trim() !== '') {
      return r[1].trim();
    }
  }

  return null;
}
<commit_msg>add fix for get _search<commit_after>var
  _ = require('lodash'),
  q = require('q'),
  stringify = require('json-stable-stringify'),
  Router = require('router'),
  bodyParser = require('body-parser'),
  finalhandler = require('finalhandler'),
  RequestObject = require('../core/models/requestObject'),
  ResponseObject = require('../core/models/responseObject'),
  PluginImplementationError = require('../core/errors/pluginImplementationError'),
  BadRequestError = require('../core/errors/badRequestError');

module.exports = function RouterController (kuzzle) {
  this.router = null;
  this.pluginRouter = null;
  this.routename = 'kuzzle';
  this.kuzzle = kuzzle;
  this.connections = {};

  /**
   * Declares a new connection on a given protocol. Called by protocol plugins.
   * Returns a context object to be used with router.execute() and router.removeConnection()
   *
   * @param {string} protocol - protocol name
   * @param {string} connectionId - unique connection identifier
   * @return {promise} connection context, to be used with other router functions
   */
  this.newConnection = function (protocol, connectionId) {
    var error;

    if (!connectionId || !protocol || typeof connectionId !== 'string' || typeof protocol !== 'string') {
      error = new PluginImplementationError('Rejected new connection declaration: invalid arguments');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(error);
    }

    if (!this.connections[connectionId]) {
      this.connections[connectionId] = {
        connection: {type: protocol, id: connectionId},
        user: null
      };
    }

    kuzzle.statistics.newConnection(this.connections[connectionId]);

    return q(this.connections[connectionId]);
  };

  /**
   * Called by protocol plugins: forward a received request to Kuzzle.
   * Return a promise resolved or rejected with the corresponding ResponseObject
   *
   * A note about the JWT headers: if this value is falsey, if no "authorization" field is found, if the token is not
   * properly formatted or if the token itself is invalid, then the corresponding user will automatically
   * be set to "anonymous".
   *
   * @param {Object} jwtHeaders - Optional JWT headers
   * @param {Object} requestObject - the request to execute
   * @param {String} context - connection context, obtained using the newConnection() method
   * @return {Promise} ResponseObject
   */
  this.execute = function (jwtHeaders, requestObject, context) {
    var error;

    if (!requestObject) {
      error = new PluginImplementationError('Request execution error: no provided request');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(new ResponseObject({}, error));
    }

    if (!context || !context.connection) {
      error = new PluginImplementationError('Unable to execute request: ' + requestObject +
        '\nReason: invalid context. Use context.getRouter().newConnection() to get a valid context.');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(new ResponseObject({}, error));
    }

    if (!context.connection.id || !this.connections[context.connection.id]) {
      error = new PluginImplementationError('Unable to execute request: unknown context. ' +
        'Has context.getRouter().newConnection() been called before executing requests?');
      kuzzle.pluginsManager.trigger('log:error', error);
      return q.reject(new ResponseObject({}, error));
    }

    return kuzzle.repositories.user.loadFromToken(getBearerTokenFromHeaders(jwtHeaders))
      .then(user => {
        context.user = user;

        return kuzzle.funnel.execute(requestObject, context);
      })
      .then(response => {
        return response;
      })
      .catch(e => {
        kuzzle.pluginsManager.trigger('log:error', e);
        return q.reject(new ResponseObject({}, e));
      });
  };

  /**
   * Called by protocol plugins: removes a connection from the connection pool.
   * @param {object} context - connection context, obtained using the newConnection() method
   */
  this.removeConnection = function (context) {
    if (context.connection.id && this.connections[context.connection.id]) {
      delete this.connections[context.connection.id];
      kuzzle.hotelClerk.removeCustomerFromAllRooms(context.connection);
      kuzzle.statistics.dropConnection(context.connection);
    }
    else {
      kuzzle.pluginsManager.trigger('log:error', new PluginImplementationError('Unable to remove connection: ' +
        context + '.\nReason: unknown context'));
    }
  };

  /**
   * Initializes the HTTP routes for the Kuzzle REST API.
   */
  this.initRouterHttp = function () {
    var
      apiBase = new Router(),
      api = new Router(),
      coverage,
      routes = [
        {verb: 'get', url: '/_getLastStats', controller: 'admin', action: 'getLastStats'},
        {verb: 'get', url: '/_getAllStats', controller: 'admin', action: 'getAllStats'},
        {verb: 'get', url: '/_now', controller: 'read', action: 'now'},
        {verb: 'get', url: '/_listIndexes', controller: 'read', action: 'listIndexes'},
        {verb: 'get', url: '/_listSubscriptions', controller: 'subscribe', action: 'list'},
        {verb: 'get', url: '/:index/_listCollections', controller: 'read', action: 'listCollections'},
        {verb: 'get', url: '/:index/_listCollections/:type', controller: 'read', action: 'listCollections'},
        {verb: 'get', url: '/:index/:collection/_mapping', controller: 'admin', action: 'getMapping'},
        {verb: 'get', url: '/:index/:collection/:id', controller: 'read', action: 'get'},

        {verb: 'post', url: '/_bulk', controller: 'bulk', action: 'import'},
        {verb: 'post', url: '/_getStats', controller: 'admin', action: 'getStats'},
        {verb: 'post', url: '/_login', controller: 'auth', action: 'login'},
        {verb: 'post', url: '/:index/_bulk', controller: 'bulk', action: 'import'},
        {verb: 'post', url: '/:index/:collection/_bulk', controller: 'bulk', action: 'import'},
        {verb: 'post', url: '/:index/:collection/_search', controller: 'read', action: 'search'},
        {verb: 'post', url: '/:index/:collection/_count', controller: 'read', action: 'count'},
        {verb: 'post', url: '/:index/:collection/_create', controller: 'write', action: 'create'},
        {verb: 'post', url: '/:index/:collection', controller: 'write', action: 'publish'},

        {verb: 'delete', url: '/_deleteIndexes', controller: 'admin', action: 'deleteIndexes'},
        {verb: 'delete', url: '/:index', controller: 'admin', action: 'deleteIndex'},
        {verb: 'delete', url: '/:index/:collection/_query', controller: 'write', action: 'deleteByQuery'},
        {verb: 'delete', url: '/:index/:collection/_truncate', controller: 'admin', action: 'truncateCollection'},
        {verb: 'delete', url: '/:index/:collection/:id', controller: 'write', action: 'delete'},
        {verb: 'delete', url: '/:index/:collection', controller: 'admin', action: 'deleteCollection'},

        {verb: 'put', url: '/_role/:id', controller: 'admin', action: 'putRole'},
        {verb: 'put', url: '/:index', controller: 'admin', action: 'createIndex'},
        {verb: 'put', url: '/:index/:collection', controller: 'write', action: 'createCollection'},
        {verb: 'put', url: '/:index/:collection/_mapping', controller: 'admin', action: 'putMapping'},
        {verb: 'put', url: '/:index/:collection/:id/_:action', controller: 'write'},
        {verb: 'put', url: '/:index/:collection/:id', controller: 'write', action: 'createOrUpdate'}
      ];
    routes = routes.concat(kuzzle.pluginsManager.routes);

    this.router = new Router();

    // create and mount a new router for the coverage API
    if (process.env.FEATURE_COVERAGE === '1') {
      coverage = require('istanbul-middleware');
      this.router.use('/coverage', coverage.createHandler({ resetOnGet: true }));
    }

    // create and mount a new router for our API
    this.router.use('/api', apiBase);
    this.router.use('/api/' + kuzzle.config.apiVersion, api);

    /*
     Registering the basic _serverInfo route
     This route is also used to get Kuzzle API Version, so it isn't registered under api/<version> but
     directly under api/
     */
    apiBase.get('/_serverInfo', (request, response) => {
      executeFromRest.call(kuzzle, {controller: 'read', action: 'serverInfo'}, request, response);
    });

    // create and mount a new router for plugins
    this.pluginRouter = new Router();
    api.use('/_plugin', this.pluginRouter);

    // add a body parsing middleware to our API
    api.use(bodyParser.json());

    // Simple hello world to let know to the user that kuzzle is running
    api.get('/', (request, response) => {
      response.writeHead('Access-Control-Allow-Origin', '*');
      response.writeHead('Access-Control-Allow-Headers', 'X-Requested-With');
      response.writeHead(200, {'Content-Type': 'application/json'});
      response.end(stringify({status: 200, error: null, result: 'Hello from Kuzzle :)'}));
    });

    // Register API routes
    routes.forEach(route => {
      api[route.verb](route.url, function (request, response) {
        var params = {
          controller: route.controller
        };

        if (route.action) {
          params.action = route.action;
        }

        executeFromRest.call(kuzzle, params, request, response);
      });
    });
  };

  /**
   * Forward incoming REST requests to the HTTP routes created by the
   * initRouterHttp function.
   *
   * @param request transmitted through the REST API
   * @param response is the HTTP connection handler
   */
  this.routeHttp = function (request, response) {
    kuzzle.pluginsManager.trigger('log:silly', 'Handle HTTP request');

    this.router(request, response, finalhandler(request, response));
  };

  /**
   * Handles requests coming from MQ protocols: AMQP, MQTT & STOMP
   *
   */
  this.routeMQListener = function () {
    kuzzle.services.list.mqBroker.listenExchange(this.routename, function handleMQMessage(msg) {
      var
        context = {
          connection: null,
          user: null
        },
        data,
        requestObject,
        rawContent;

      if (!(msg.content instanceof Buffer)) {
        rawContent = msg.content.toString();
      }
      else {
        rawContent = (new Buffer(msg.content)).toString();
      }

      try {
        data = JSON.parse(rawContent);
      }
      catch (e) {
        kuzzle.pluginsManager.trigger('log:error', {message: 'Parse error', error: e});
        return false;
      }

      kuzzle.pluginsManager.trigger('log:silly', 'Handle MQ input' + msg.fields.routingKey);

      // For MQTT messages, we do not have a replyTo header like with AMQP or STOMP
      // => MQTT client has to send its mqtt client id and subscribe to the topic exchange mqtt.<clientId>
      //    to get feedback from Kuzzle.
      if (msg.properties && msg.properties.replyTo) {
        context.connection = {type: 'amq', id: data.clientId};
      }
      else {
        context.connection = {type: 'mqtt', id: data.clientId};
      }

      requestObject = new RequestObject(data, {}, 'mq');

      kuzzle.repositories.user.loadFromToken(getBearerTokenFromHeaders(data.headers))
        .then(function (user) {
          context.user = user;
          return kuzzle.funnel.execute(requestObject, context);
        })
        .then(function (responseObject) {
          if (context.connection.type === 'amq') {
            kuzzle.services.list.mqBroker.replyTo(msg.properties.replyTo, responseObject.toJson());
          }
          else {
            kuzzle.services.list.mqBroker.addExchange('mqtt.' + context.connection.id, responseObject.toJson());
          }
        })
        .catch(function (error) {
          var errorObject = new ResponseObject({}, error);
          kuzzle.pluginsManager.trigger('log:error', error);
          if (context.connection.type === 'amq') {
            kuzzle.services.list.mqBroker.replyTo(msg.properties.replyTo, errorObject.toJson());
          }
          else {
            kuzzle.services.list.mqBroker.addExchange('mqtt.' + context.connection.id, errorObject.toJson());
          }
        });
    });
  };

};

/**
 * Transmit HTTP requests to the funnel controller and forward its response back to
 * the client
 *
 * @param params contains the request metadata
 * @param request is the original request from the client
 * @param response is the HTTP connection handler
 */
function executeFromRest(params, request, response) {
  var
    requestObject,
    errorObject,
    data,
    context = {
      connection: {type: 'rest'},
      user: null
    };

  if (!params.controller) {
    errorObject = new ResponseObject({}, new BadRequestError('The "controller" argument is missing'));
    response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
    response.end(stringify(errorObject.toJson()));
    return false;
  }

  if (request.headers['content-type'] && request.headers['content-type'] !== 'application/json') {
    errorObject = new ResponseObject({}, new BadRequestError('Invalid request content-type. Expected "application/json", got: "' + request.headers['content-type'] +'"'));
    response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
    response.end(stringify(errorObject.toJson()));
    return false;
  }

  data = {
    controller: params.controller,
    action: params.action || request.params.action,
    collection: request.params.collection
  };

  if (request.params.action) {
    delete request.params.action;
  }
  if (request.params.collection) {
    delete request.params.collection;
  }
  if (request.params.id) {
    data._id = request.params.id;
    delete request.params.id;
  }

  if (request.params.index) {
    data.index = request.params.index;
    delete request.params.index;
  }

  _.forEach(request.params, function (value, param) {
    request.body[param] = value;
  });

  requestObject = new RequestObject(data, request.body, 'rest');

  response.setHeader('Access-Control-Allow-Origin', '*');
  response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With');

  // Just in case the user make a GET on url /mainindex/test/_search
  // Without this test we return something weird: a result.hits.hits with all document without filter
  if (data.controller === 'read' && data.action === 'get') {
    if (['_search', '_bulk', '_count', '_create', '_query', '_truncate'].indexOf(data._id) !== -1) {
      errorObject = new BadRequestError('The action ' + requestObject.data._id + ' can\'t be done with a GET');
      response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
      response.end(stringify(errorObject.toJson()));

      return false;
    }
  }

  this.repositories.user.loadFromToken(getBearerTokenFromHeaders(request.headers))
    .then(function (user) {
      context.user = user;

      return this.funnel.execute(requestObject, context);
    }.bind(this))
    .then(function (responseObject) {
      response.writeHead(responseObject.status, {'Content-Type': 'application/json'});
      response.end(stringify(responseObject.toJson()));
    })
    .catch(function (error) {
      errorObject = new ResponseObject(requestObject, error);
      this.emit(requestObject.controller + ':websocket:funnel:reject', error);
      this.pluginsManager.trigger('log:error', error);
      response.writeHead(errorObject.status, {'Content-Type': 'application/json'});
      response.end(stringify(errorObject.toJson()));
    }.bind(this));
}

/**
 * Extract the Bearer token from the given headers
 * @param {Object} headers
 * @returns {*}
 */
function getBearerTokenFromHeaders (headers) {
  var
    r;

  if (headers !== undefined && headers.authorization !== undefined) {
    r = /^Bearer (.+)$/.exec(headers.authorization);
    if (r !== null && r[1].trim() !== '') {
      return r[1].trim();
    }
  }

  return null;
}
<|endoftext|>


<commit_before>/**
 * class Array
 * A standard JavaScript array.
 **/

/**
 * Jax.Util
 * Contains general-purpose utility and helper functions
 **/
Jax.Util = {
  findMaterial: function(name_or_instance) {
    if (typeof(name_or_instance) == "string")
      return Jax.Material.find(name_or_instance);
    else if (name_or_instance instanceof Jax.Material)
      return name_or_instance;

    throw new Error("Material must be an instance of Jax.Material, or "+
                    "a string representing a material in the Jax material registry");
  },

  scan: function(str, end, incr, decr, startIndex, singleLineComment, multiLineCommentStart, multiLineCommentEnd) {
    end = end || ')';
    incr = incr || '(';
    decr = decr || ')';
    singleLineComment = singleLineComment || "//";
    multiLineCommentStart = multiLineCommentStart || "/*";
    multiLineCommentEnd   = multiLineCommentEnd   || "*/";

    startIndex = startIndex || 0;
    var depth = 0;
    var result = "";
    var inComment = 0;
    for (var i = startIndex; i < str.length; i++) {
      var ch = str[i];
      switch(ch) {
        case incr: if (!inComment) depth++; break;
        case decr: if (!inComment) depth--; break;
      }
      if (depth < 0) break;
      result += ch;

      if (result.length >= singleLineComment.length &&
          result.substring(result.length - singleLineComment.length, result.length) === singleLineComment)
        inComment = 1;
      if (inComment === 1 && ch === "\n")
        inComment = 0;
      if (!inComment && result.length >= multiLineCommentStart.length &&
          result.substring(result.length - multiLineCommentStart.length, result.length) === multiLineCommentStart)
        inComment = 2;
      if (inComment === 2 && result.length >= multiLineCommentEnd.length &&
          result.substring(result.length - multiLineCommentEnd.length, result.length) === multiLineCommentEnd)
        inComment = 0;
    }
    return result;
  },

  gsub: function(source, pattern, replacement) {
    var match, result, submatch, _replacement;
    if (!((pattern != null) && (replacement != null))) {
      return source;
    }
    result = '';
    while (source.length > 0) {
      if ((match = source.match(pattern))) {
        // handle \1, \2, etc in replacement string
        _replacement = replacement;
        while (submatch = _replacement.match(/\\(\d+)/)) {
          _replacement = _replacement.replace('\\'+submatch[1], match[submatch[1]]);
        }

        result += source.slice(0, match.index);
        result += _replacement;
        source = source.slice(match.index + match[0].length);
      } else {
        result += source;
        source = '';
      }
    }
    return result;
  },

  /**
   * Jax.Util.underscore(word) -> String
   * word (String): a String to be converted to underscore.
   *
   * Takes a String, which may be in CamelCase format, and returns
   * the same string converted to underscored_format. Examples:
   *
   *     "HelloWorld" => "hello_world"
   *     "Hello_World" => "hello_world"
   *     "Hello" => "hello"
   *
   **/
  underscore: function(word) {
    word = Jax.Util.gsub(word, /::/, "\/");
    word = Jax.Util.gsub(word, /([A-Z]+)([A-Z][a-z])/,'\\1_\\2');
    word = Jax.Util.gsub(word, /([a-z\d])([A-Z])/,'\\1_\\2');

    while (word.indexOf('-') != -1) word = word.replace('-', '_');

    return word.toLowerCase();
  },

  /**
   * Jax.Util.decodePickingColor(red, green, blue, alpha) -> Number
   *
   * Performs the reverse of the 'picking' shader by restoring a number
   * that was previously encoded into the four color channels.
   **/
  decodePickingColor: function(red, green, blue, alpha) {
    /* blue is a key. It is always max. So if it's 1, we're dealing with floats; else, bytes. */
    if (blue == 1.0) {
      red *= 255;
      green *= 255;
    }
    return red * 256 + green;
  },

  /**
   * Jax.Util.vectorize(object) -> vec3
   * - object (Object): any Object
   *
   * Analyzes the input object and returns a vec3 based on its contents. Input can be any of the following:
   *   * a vec3
   *   * an array
   *   * an object with {x, y, z} properties
   *   * an object with {0, 1, 2} properties
   *   * a string delimited with any combination of commas, spaces and/or tab characters. Examples:
   *     "x,y,z"
   *     "x y z"
   *     "x, y, z"
   *     "x\ty\tz"
   *     "x, \ty, \tz"
   **/
  vectorize: function(data) {
    if (data) {
      var res = [];//vec3.create();
      if (typeof(data) == "string") {
        var components = data.split(/[,\s]+/);
        if (components.length >= 2) {
          for (var i = 0; i < components.length; i++)
            res[i] = parseFloat(components[i]);
        }
        return res;
      }
      if (data.length && data.length >= 3) return vec3.set(data, res);
      else if (data.length == 2) return [data[0], data[1]];
      if ((res[0] = data.x) != undefined && (res[1] = data.y) != undefined) {
        if (data.z != undefined) res[2] = data.z;
        return res;
      }
      if ((res[0] = data[0]) != undefined && (res[1] = data[1]) != undefined) {
        if (data[2] != undefined) res[2] = data[2];
        return res;
      }
    }
    throw new Error("Input argument for Jax.Util.vectorize not recognized: "+JSON.stringify(data));
  },

  /**
   * Jax.Util.colorize(object) -> vec4
   * - object (Object): any Object
   *
   * Analyzes the input object and returns a 4-component color vector based on its contents. Input can be any of the
   * following:
   *   * an array
   *   * an object with {r, g, b, a} properties
   *   * an object with {0, 1, 2, 3} properties
   *   * a string delimited with any combination of commas, spaces and/or tab characters. Examples:
   *     "r,g,b,a"
   *     "r g b a"
   *     "r, g, b, a"
   *     "r\tg\tb\ta"
   *     "r, \tg, \tb, \ta"
   *
   * In all cases, if the alpha component is omitted, it defaults to 1.0.
   **/
  colorize: function(data) {
    if (data) {
      var res = [0,0,0,0];

      if (typeof(data) == "string") {
        var components = data.split(/[,\s]+/);
        if (components.length >= 3) {
          for (var i = 0; i < 4; i++)
            if (components.length <= i)
              res[i] = 1.0;
            else
              res[i] = parseFloat(components[i]);
        }
        return res;
      }
      if (data.length && data.length >= 3) {
        res[0] = data[0];
        res[1] = data[1];
        res[2] = data[2];
        if ((res[3] = data[3]) == undefined) res[3] = 1.0;
        else res[3] = 1.0;
        return res;
      }
      if ((res[0] = data.r) != undefined && (res[1] = data.g) != undefined && (res[2] = data.b) != undefined) {
        if ((res[3] = data.a) == undefined) res[3] = 1.0;
        return res;
      }
      if ((res[0] = data.red) != undefined && (res[1] = data.green) != undefined && (res[2] = data.blue) != undefined) {
        if ((res[3] = data.alpha) == undefined) res[3] = 1.0;
        return res;
      }
      if ((res[0] = data[0]) != undefined && (res[1] = data[1]) != undefined && (res[2] = data[2]) != undefined) {
        if ((res[3] = data[3]) == undefined) res[3] = 1.0;
        return res;
      }
    }
    throw new Error("Input argument for Jax.Util.colorize not recognized: "+JSON.stringify(data));
  },

  /**
   * Jax.Util.properties(object) -> Array
   * - object (Object): any Object
   *
   * Returns an array containing the names of all properties in the specified object.
   **/
  properties: function(object) {
    var arr = [];
    for (var i in object)
      arr.push(i);
    return arr;
  },

  /**
   * Jax.Util.merge(incoming, outgoing) -> outgoing
   * Merges the two objects by copying all properties from _incoming_ into _outgoing_, replacing
   * any properties that already exist. _outgoing_ will retain any properties that do not exist
   * in _incoming_.
   **/
  merge: function(src, dst) {
    if (!src) return dst;
    var i, j, n;

    function doComparison(i) {
      if (src[i] == null) dst[i] = null;
      else if (src[i].klass)           dst[i] = src[i];
      else if (Object.isArray(src[i])) Jax.Util.merge(src[i], dst[i] = dst[i] || []);
      else if (typeof(src[i]) == "object") {
        if (Object.isArray(dst[i])) {
          n = {};
          for (j = 0; j < dst[i].length; j++) n[j] = dst[i][j];
          dst[i] = n;
        }
        Jax.Util.merge(src[i], dst[i] = dst[i] || {});
      }
      else dst[i] = src[i];
    }

    if (Object.isArray(src)) for (i = 0; i < src.length; i++) doComparison(i);
    else for (i in src) doComparison(i);

    return dst;
  },

  /**
   * Jax.Util.normalizeOptions(incoming, defaults) -> Object
   * Receives incoming and formats it into a generic Object with a structure representing the given defaults.
   * The returned object is always a brand-new object, to avoid polluting original incoming object.
   * If the object contains a Jax.Class instance, that actual object is copied over. All other objects
   * are cloned into brand-new objects.
   **/
  normalizeOptions: function(incoming, defaults) {
    // throw new Error("Jax.Util.normalizeOptions is being phased out of core (with "+JSON.stringify(incoming)+" and "+JSON.stringify(defaults)+")");
    var result = {};
    Jax.Util.merge(defaults, result);
    Jax.Util.merge(incoming, result);
    return result;
  },

  /**
   * Jax.Util.sizeofFormat(glEnum) -> Number
   *
   * Returns the byte size of an array consisting of this type of element.
   *
   * Example:
   *
   *     Jax.Util.sizeofFormat(GL_RGB)
   *     //=> 3
   *
   * If this isn't a recognized format, it is passed off to Jax.Util.enumName and an error is thrown.
   **/
  sizeofFormat: function(glEnum) {
    switch(glEnum) {
      case GL_ALPHA: return 1;           // alpha component only
      case GL_LUMINANCE: return 1;       // luminance component only
      case GL_RGB: return 3;             // RGB triplet
      case GL_RGBA: return 4;            // all 4 components
      case GL_LUMINANCE_ALPHA: return 2; // luminance/alpha pair
    }
    throw new Error("Unrecognized format: "+Jax.Util.enumName(glEnum));
  },

  /**
   * Jax.Util.enumName(glEnum) -> String
   * - glEnum (GLenum): a WebGL enumeration, presumably numeric.
   *
   * Returns the name of the enumeration prefixed with "GL_" that shares a value with the passed-in enum.
   *
   * If the enum can't be found (this happens sometimes when an enum crops up that isn't in the WebGL spec),
   * then a string containing both the decimal and hexadecimal form of this enum is returned:
   *
   *     "(unrecognized enum: 36059 [0x8cdb])"
   *
   * This is primarily for debugging and error reporting.
   **/
  enumName: function(glEnum) {
    var global = Jax.getGlobal();
    for (var i in global) {
      if (i.indexOf("GL_") == 0 && global[i] == glEnum)
        return i;
    }
    return "(unrecognized enum: "+glEnum+" [0x"+parseInt(glEnum).toString(16)+"])";
  },

  /**
   * Jax.Util.addRequestedHelpers(klass) -> Array
   * - klass (Jax.Class): A class into which to mix the helpers.
   *
   * First, if +ApplicationHelper+ is defined, it is automatically mixed into the specified class.
   *
   * Then, the class is searched for a #helpers method; if it exists, it is expected to return an array of
   * Helpers (created with +Jax.Helper.create({...})+ ). Each element in the array returned by #helpers is
   * then mixed into the class.
   *
   * An array of all helpers that were just mixed into the target class is returned.
   *
   * As of Jax v1.1.0, you may also set the array of helpers directly on the +helpers+ property
   * of a class, instead of defining a function.
   **/
  addRequestedHelpers: function(klass) {
    var helpers = [];
    if (typeof(ApplicationHelper) != "undefined") {
      helpers.push(ApplicationHelper);
      klass.addMethods(ApplicationHelper);
    }
    if (klass.prototype.helpers) {
      var helper_array;
      if (typeof(klass.prototype.helpers) == "function")
        helper_array = klass.prototype.helpers.call(klass);
      else helper_array = klass.prototype.helpers;

      for (var i = 0; i < helper_array.length; i++) {
        helpers.push(helper_array[i]);
        klass.addMethods(helper_array[i]);
      }
    }
    return helpers;
  }
};
<commit_msg>give names to merged and normalized objects for easier debugging<commit_after>/**
 * class Array
 * A standard JavaScript array.
 **/

/**
 * Jax.Util
 * Contains general-purpose utility and helper functions
 **/
Jax.Util = {
  findMaterial: function(name_or_instance) {
    if (typeof(name_or_instance) == "string")
      return Jax.Material.find(name_or_instance);
    else if (name_or_instance instanceof Jax.Material)
      return name_or_instance;

    throw new Error("Material must be an instance of Jax.Material, or "+
                    "a string representing a material in the Jax material registry");
  },

  scan: function(str, end, incr, decr, startIndex, singleLineComment, multiLineCommentStart, multiLineCommentEnd) {
    end = end || ')';
    incr = incr || '(';
    decr = decr || ')';
    singleLineComment = singleLineComment || "//";
    multiLineCommentStart = multiLineCommentStart || "/*";
    multiLineCommentEnd   = multiLineCommentEnd   || "*/";

    startIndex = startIndex || 0;
    var depth = 0;
    var result = "";
    var inComment = 0;
    for (var i = startIndex; i < str.length; i++) {
      var ch = str[i];
      switch(ch) {
        case incr: if (!inComment) depth++; break;
        case decr: if (!inComment) depth--; break;
      }
      if (depth < 0) break;
      result += ch;

      if (result.length >= singleLineComment.length &&
          result.substring(result.length - singleLineComment.length, result.length) === singleLineComment)
        inComment = 1;
      if (inComment === 1 && ch === "\n")
        inComment = 0;
      if (!inComment && result.length >= multiLineCommentStart.length &&
          result.substring(result.length - multiLineCommentStart.length, result.length) === multiLineCommentStart)
        inComment = 2;
      if (inComment === 2 && result.length >= multiLineCommentEnd.length &&
          result.substring(result.length - multiLineCommentEnd.length, result.length) === multiLineCommentEnd)
        inComment = 0;
    }
    return result;
  },

  gsub: function(source, pattern, replacement) {
    var match, result, submatch, _replacement;
    if (!((pattern != null) && (replacement != null))) {
      return source;
    }
    result = '';
    while (source.length > 0) {
      if ((match = source.match(pattern))) {
        // handle \1, \2, etc in replacement string
        _replacement = replacement;
        while (submatch = _replacement.match(/\\(\d+)/)) {
          _replacement = _replacement.replace('\\'+submatch[1], match[submatch[1]]);
        }

        result += source.slice(0, match.index);
        result += _replacement;
        source = source.slice(match.index + match[0].length);
      } else {
        result += source;
        source = '';
      }
    }
    return result;
  },

  /**
   * Jax.Util.underscore(word) -> String
   * word (String): a String to be converted to underscore.
   *
   * Takes a String, which may be in CamelCase format, and returns
   * the same string converted to underscored_format. Examples:
   *
   *     "HelloWorld" => "hello_world"
   *     "Hello_World" => "hello_world"
   *     "Hello" => "hello"
   *
   **/
  underscore: function(word) {
    word = Jax.Util.gsub(word, /::/, "\/");
    word = Jax.Util.gsub(word, /([A-Z]+)([A-Z][a-z])/,'\\1_\\2');
    word = Jax.Util.gsub(word, /([a-z\d])([A-Z])/,'\\1_\\2');

    while (word.indexOf('-') != -1) word = word.replace('-', '_');

    return word.toLowerCase();
  },

  /**
   * Jax.Util.decodePickingColor(red, green, blue, alpha) -> Number
   *
   * Performs the reverse of the 'picking' shader by restoring a number
   * that was previously encoded into the four color channels.
   **/
  decodePickingColor: function(red, green, blue, alpha) {
    /* blue is a key. It is always max. So if it's 1, we're dealing with floats; else, bytes. */
    if (blue == 1.0) {
      red *= 255;
      green *= 255;
    }
    return red * 256 + green;
  },

  /**
   * Jax.Util.vectorize(object) -> vec3
   * - object (Object): any Object
   *
   * Analyzes the input object and returns a vec3 based on its contents. Input can be any of the following:
   *   * a vec3
   *   * an array
   *   * an object with {x, y, z} properties
   *   * an object with {0, 1, 2} properties
   *   * a string delimited with any combination of commas, spaces and/or tab characters. Examples:
   *     "x,y,z"
   *     "x y z"
   *     "x, y, z"
   *     "x\ty\tz"
   *     "x, \ty, \tz"
   **/
  vectorize: function(data) {
    if (data) {
      var res = [];//vec3.create();
      if (typeof(data) == "string") {
        var components = data.split(/[,\s]+/);
        if (components.length >= 2) {
          for (var i = 0; i < components.length; i++)
            res[i] = parseFloat(components[i]);
        }
        return res;
      }
      if (data.length && data.length >= 3) return vec3.set(data, res);
      else if (data.length == 2) return [data[0], data[1]];
      if ((res[0] = data.x) != undefined && (res[1] = data.y) != undefined) {
        if (data.z != undefined) res[2] = data.z;
        return res;
      }
      if ((res[0] = data[0]) != undefined && (res[1] = data[1]) != undefined) {
        if (data[2] != undefined) res[2] = data[2];
        return res;
      }
    }
    throw new Error("Input argument for Jax.Util.vectorize not recognized: "+JSON.stringify(data));
  },

  /**
   * Jax.Util.colorize(object) -> vec4
   * - object (Object): any Object
   *
   * Analyzes the input object and returns a 4-component color vector based on its contents. Input can be any of the
   * following:
   *   * an array
   *   * an object with {r, g, b, a} properties
   *   * an object with {0, 1, 2, 3} properties
   *   * a string delimited with any combination of commas, spaces and/or tab characters. Examples:
   *     "r,g,b,a"
   *     "r g b a"
   *     "r, g, b, a"
   *     "r\tg\tb\ta"
   *     "r, \tg, \tb, \ta"
   *
   * In all cases, if the alpha component is omitted, it defaults to 1.0.
   **/
  colorize: function(data) {
    if (data) {
      var res = [0,0,0,0];

      if (typeof(data) == "string") {
        var components = data.split(/[,\s]+/);
        if (components.length >= 3) {
          for (var i = 0; i < 4; i++)
            if (components.length <= i)
              res[i] = 1.0;
            else
              res[i] = parseFloat(components[i]);
        }
        return res;
      }
      if (data.length && data.length >= 3) {
        res[0] = data[0];
        res[1] = data[1];
        res[2] = data[2];
        if ((res[3] = data[3]) == undefined) res[3] = 1.0;
        else res[3] = 1.0;
        return res;
      }
      if ((res[0] = data.r) != undefined && (res[1] = data.g) != undefined && (res[2] = data.b) != undefined) {
        if ((res[3] = data.a) == undefined) res[3] = 1.0;
        return res;
      }
      if ((res[0] = data.red) != undefined && (res[1] = data.green) != undefined && (res[2] = data.blue) != undefined) {
        if ((res[3] = data.alpha) == undefined) res[3] = 1.0;
        return res;
      }
      if ((res[0] = data[0]) != undefined && (res[1] = data[1]) != undefined && (res[2] = data[2]) != undefined) {
        if ((res[3] = data[3]) == undefined) res[3] = 1.0;
        return res;
      }
    }
    throw new Error("Input argument for Jax.Util.colorize not recognized: "+JSON.stringify(data));
  },

  /**
   * Jax.Util.properties(object) -> Array
   * - object (Object): any Object
   *
   * Returns an array containing the names of all properties in the specified object.
   **/
  properties: function(object) {
    var arr = [];
    for (var i in object)
      arr.push(i);
    return arr;
  },

  /**
   * Jax.Util.merge(incoming, outgoing) -> outgoing
   * Merges the two objects by copying all properties from _incoming_ into _outgoing_, replacing
   * any properties that already exist. _outgoing_ will retain any properties that do not exist
   * in _incoming_.
   **/
  merge: function(src, dst) {
    if (!src) return dst;
    var i, j, n;

    function doComparison(i) {
      if (src[i] == null) dst[i] = null;
      else if (src[i].klass)           dst[i] = src[i];
      else if (Object.isArray(src[i])) Jax.Util.merge(src[i], dst[i] = dst[i] || []);
      else if (typeof(src[i]) == "object") {
        if (Object.isArray(dst[i])) {
          n = {};
          for (j = 0; j < dst[i].length; j++) n[j] = dst[i][j];
          dst[i] = n;
        }
        Jax.Util.merge(src[i], dst[i] = dst[i] || new (function MergedObject() { })());
      }
      else dst[i] = src[i];
    }

    if (Object.isArray(src)) for (i = 0; i < src.length; i++) doComparison(i);
    else for (i in src) doComparison(i);

    return dst;
  },

  /**
   * Jax.Util.normalizeOptions(incoming, defaults) -> Object
   * Receives incoming and formats it into a generic Object with a structure representing the given defaults.
   * The returned object is always a brand-new object, to avoid polluting original incoming object.
   * If the object contains a Jax.Class instance, that actual object is copied over. All other objects
   * are cloned into brand-new objects.
   **/
  normalizeOptions: function(incoming, defaults) {
    // throw new Error("Jax.Util.normalizeOptions is being phased out of core (with "+JSON.stringify(incoming)+" and "+JSON.stringify(defaults)+")");
    var result = new (function NormalizedObject() { })();
    Jax.Util.merge(defaults, result);
    Jax.Util.merge(incoming, result);
    return result;
  },

  /**
   * Jax.Util.sizeofFormat(glEnum) -> Number
   *
   * Returns the byte size of an array consisting of this type of element.
   *
   * Example:
   *
   *     Jax.Util.sizeofFormat(GL_RGB)
   *     //=> 3
   *
   * If this isn't a recognized format, it is passed off to Jax.Util.enumName and an error is thrown.
   **/
  sizeofFormat: function(glEnum) {
    switch(glEnum) {
      case GL_ALPHA: return 1;           // alpha component only
      case GL_LUMINANCE: return 1;       // luminance component only
      case GL_RGB: return 3;             // RGB triplet
      case GL_RGBA: return 4;            // all 4 components
      case GL_LUMINANCE_ALPHA: return 2; // luminance/alpha pair
    }
    throw new Error("Unrecognized format: "+Jax.Util.enumName(glEnum));
  },

  /**
   * Jax.Util.enumName(glEnum) -> String
   * - glEnum (GLenum): a WebGL enumeration, presumably numeric.
   *
   * Returns the name of the enumeration prefixed with "GL_" that shares a value with the passed-in enum.
   *
   * If the enum can't be found (this happens sometimes when an enum crops up that isn't in the WebGL spec),
   * then a string containing both the decimal and hexadecimal form of this enum is returned:
   *
   *     "(unrecognized enum: 36059 [0x8cdb])"
   *
   * This is primarily for debugging and error reporting.
   **/
  enumName: function(glEnum) {
    var global = Jax.getGlobal();
    for (var i in global) {
      if (i.indexOf("GL_") == 0 && global[i] == glEnum)
        return i;
    }
    return "(unrecognized enum: "+glEnum+" [0x"+parseInt(glEnum).toString(16)+"])";
  },

  /**
   * Jax.Util.addRequestedHelpers(klass) -> Array
   * - klass (Jax.Class): A class into which to mix the helpers.
   *
   * First, if +ApplicationHelper+ is defined, it is automatically mixed into the specified class.
   *
   * Then, the class is searched for a #helpers method; if it exists, it is expected to return an array of
   * Helpers (created with +Jax.Helper.create({...})+ ). Each element in the array returned by #helpers is
   * then mixed into the class.
   *
   * An array of all helpers that were just mixed into the target class is returned.
   *
   * As of Jax v1.1.0, you may also set the array of helpers directly on the +helpers+ property
   * of a class, instead of defining a function.
   **/
  addRequestedHelpers: function(klass) {
    var helpers = [];
    if (typeof(ApplicationHelper) != "undefined") {
      helpers.push(ApplicationHelper);
      klass.addMethods(ApplicationHelper);
    }
    if (klass.prototype.helpers) {
      var helper_array;
      if (typeof(klass.prototype.helpers) == "function")
        helper_array = klass.prototype.helpers.call(klass);
      else helper_array = klass.prototype.helpers;

      for (var i = 0; i < helper_array.length; i++) {
        helpers.push(helper_array[i]);
        klass.addMethods(helper_array[i]);
      }
    }
    return helpers;
  }
};
<|endoftext|>


<commit_before>define([
    "sprd/data/SprdModel",
    'js/core/List',
    'sprd/model/ProductType',
    'js/data/AttributeTypeResolver',
    'sprd/entity/DesignConfiguration',
    'sprd/entity/TextConfiguration',
    'sprd/entity/Appearance',
    'js/data/TypeResolver', 'js/data/Entity', "underscore", "flow", "sprd/util/ProductUtil"],
    function (SprdModel, List, ProductType, AttributeTypeResolver, DesignConfiguration, TextConfiguration, Appearance, TypeResolver, Entity, _, flow, ProductUtil) {
        return SprdModel.inherit("sprd.model.Product", {

            schema: {
                productType: ProductType,
                appearance: Appearance,
                configurations: [new AttributeTypeResolver({
                    attribute: "type",
                    mapping: {
                        "design": DesignConfiguration,
                        "text": TextConfiguration
                    }
                })]
            },

            defaults: {
                productType: null,
                appearance: null,
                view: null,
                configurations: List
            },

            price: function () {
                // TODO format price with currency
                return this.$.price.vatIncluded;
            },

            getDefaultView: function () {

                if (this.$.productType) {
                    var defaultViewId = this.getDefaultViewId();

                    if (defaultViewId !== null) {
                        return this.$.productType.getViewById(defaultViewId);
                    } else {
                        return this.$.productType.getDefaultView();
                    }
                }

            },

            getDefaultViewId: function(){
                if (this.$.defaultValues) {
                    return this.$.defaultValues.defaultView.id;
                }
                return null;
            },

            getDefaultAppearance: function() {
                if(this.$.appearance && this.$.productType){
                    return this.$.productType.getAppearanceById(this.$.appearance.$.id);
                }
            },

            _addConfiguration: function(configuration) {
                this.$.configurations.add(configuration);
            },

            addDesign: function (params, callback) {
                params = _.defaults({}, params, {
                    design: null,
                    perspective: null, // front, back, etc...
                    view: null,
                    printArea: null,
                    printType: null
                });

                var self = this,
                    design = params.design,
                    productType = this.$.productType,
                    printArea = params.printArea,
                    view = params.view,
                    appearance = this.$.appearance,
                    printType = params.printType;

                if (!design) {
                    callback(new Error("No design"));
                    return;
                }

                if (!productType) {
                    callback(new Error("ProductType not set"));
                    return;
                }

                if (!appearance) {
                    callback(new Error("Appearance for product not set"));
                    return;
                }

                flow()
                    .par(function (cb) {
                        design.fetch(null, cb);
                    }, function (cb) {
                        productType.fetch(null, cb);
                    })
                    .seq("printArea", function () {

                        if (!printArea && params.perspective && !view) {
                            view = productType.getViewByPerspective(params.perspective);
                        }

                        if (!printArea && view) {
                            // get print area by view
                            if (!productType.containsView(view)) {
                                throw new Error("View not on ProductType");
                            }

                            // TODO: look for print area that supports print types, etc...
                            printArea = view.getDefaultPrintArea();
                        }

                        view = self.$.view || self.getDefaultView();
                        if (!printArea && view) {
                            printArea = view.getDefaultPrintArea();
                        }

                        if (!printArea) {
                            throw new Error("target PrintArea couldn't be found.");
                        }

                        return printArea;
                    })
                    .seq("printType", function() {
                        var possiblePrintTypes = ProductUtil.getPossiblePrintTypesForDesignOnPrintArea(design, printArea, appearance.$.id);

                        if (printType && !_.contains(possiblePrintTypes, printType)) {
                            throw new Error("PrintType not possible for design and printArea");
                        }

                        printType = printType || possiblePrintTypes[0];

                        if (!printType) {
                            throw new Error("No printType available");
                        }

                        return printType;
                    })
                    .seq(function() {
                        var configuration = new DesignConfiguration({
                            printType: printType,
                            printArea: printArea
                        });

                        self._addConfiguration(configuration)
                    })
                    .exec(callback)

            }
        });
    });
<commit_msg>tmp. deactivated print type check<commit_after>define([
    "sprd/data/SprdModel",
    'js/core/List',
    'sprd/model/ProductType',
    'js/data/AttributeTypeResolver',
    'sprd/entity/DesignConfiguration',
    'sprd/entity/TextConfiguration',
    'sprd/entity/Appearance',
    'js/data/TypeResolver', 'js/data/Entity', "underscore", "flow", "sprd/util/ProductUtil"],
    function (SprdModel, List, ProductType, AttributeTypeResolver, DesignConfiguration, TextConfiguration, Appearance, TypeResolver, Entity, _, flow, ProductUtil) {
        return SprdModel.inherit("sprd.model.Product", {

            schema: {
                productType: ProductType,
                appearance: Appearance,
                configurations: [new AttributeTypeResolver({
                    attribute: "type",
                    mapping: {
                        "design": DesignConfiguration,
                        "text": TextConfiguration
                    }
                })]
            },

            defaults: {
                productType: null,
                appearance: null,
                view: null,
                configurations: List
            },

            price: function () {
                // TODO format price with currency
                return this.$.price.vatIncluded;
            },

            getDefaultView: function () {

                if (this.$.productType) {
                    var defaultViewId = this.getDefaultViewId();

                    if (defaultViewId !== null) {
                        return this.$.productType.getViewById(defaultViewId);
                    } else {
                        return this.$.productType.getDefaultView();
                    }
                }

            },

            getDefaultViewId: function(){
                if (this.$.defaultValues) {
                    return this.$.defaultValues.defaultView.id;
                }
                return null;
            },

            getDefaultAppearance: function() {
                if(this.$.appearance && this.$.productType){
                    return this.$.productType.getAppearanceById(this.$.appearance.$.id);
                }
            },

            _addConfiguration: function(configuration) {
                this.$.configurations.add(configuration);
            },

            addDesign: function (params, callback) {
                params = _.defaults({}, params, {
                    design: null,
                    perspective: null, // front, back, etc...
                    view: null,
                    printArea: null,
                    printType: null
                });

                var self = this,
                    design = params.design,
                    productType = this.$.productType,
                    printArea = params.printArea,
                    view = params.view,
                    appearance = this.$.appearance,
                    printType = params.printType;

                if (!design) {
                    callback(new Error("No design"));
                    return;
                }

                if (!productType) {
                    callback(new Error("ProductType not set"));
                    return;
                }

                if (!appearance) {
                    callback(new Error("Appearance for product not set"));
                    return;
                }

                flow()
                    .par(function (cb) {
                        design.fetch(null, cb);
                    }, function (cb) {
                        productType.fetch(null, cb);
                    })
                    .seq("printArea", function () {

                        if (!printArea && params.perspective && !view) {
                            view = productType.getViewByPerspective(params.perspective);
                        }

                        if (!printArea && view) {
                            // get print area by view
                            if (!productType.containsView(view)) {
                                throw new Error("View not on ProductType");
                            }

                            // TODO: look for print area that supports print types, etc...
                            printArea = view.getDefaultPrintArea();
                        }

                        view = self.$.view || self.getDefaultView();
                        if (!printArea && view) {
                            printArea = view.getDefaultPrintArea();
                        }

                        if (!printArea) {
                            throw new Error("target PrintArea couldn't be found.");
                        }

                        return printArea;
                    })
                    .seq("printType", function() {
                        var possiblePrintTypes = ProductUtil.getPossiblePrintTypesForDesignOnPrintArea(design, printArea, appearance.$.id);

                        if (printType && !_.contains(possiblePrintTypes, printType)) {
                            throw new Error("PrintType not possible for design and printArea");
                        }

                        printType = printType || possiblePrintTypes[0];

                        if (!printType) {
                            // FIXME: throw error
                            // throw new Error("No printType available");
                        }

                        return printType;
                    })
                    .seq(function() {
                        var configuration = new DesignConfiguration({
                            printType: printType,
                            printArea: printArea
                        });

                        self._addConfiguration(configuration)
                    })
                    .exec(callback)

            }
        });
    });
<|endoftext|>