<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * PropertySet facilitates creation and use of multiple named Property instances.  There are still several API design issues in question, but this
 * class is ready for use.
 *
 * A PropertySet is a set of Property instances that provides support for:
 * -Easily creating several properties using an object literal (hash)
 * -Resetting them as a group
 * -Set multiple values at once, using propertySet.set({x:100,y:200,name:'alice'});
 * -Support for derived properties, which appear with the same interface as basic properties
 * -Convenient toString that prints e.g., PropertySet{name:'larry',age:101,kids:['alice','bob']}
 * -Wiring up to listen to multiple properties simultaneously
 * -Add properties after the PropertySet is created?  Don't forget to add to the key list as well.
 * -Remove properties that were added using addProperty or the constructor
 * -TODO: Make it easy to mix-in with model classes?  Subclassing PropertySet already works fairly well, so this may good enough already.
 * -TODO: Type checking, so that a boolean input will be automatically generated as BooleanProperty, etc.
 *
 * Sample usage:
 * var p = new PropertySet( {name: 'larry', age: 100, kids: ['alice', 'bob']} );
 * p.nameProperty.link( function( n ) {console.log( 'hello ' + n );} );
 * p.name = 'jensen';
 * p.age = 101;//Happy Birthday!
 * console.log( p );
 * p.reset();
 * console.log( p );
 * p.set({name:'clark',age:102,kids:['alice','bob','charlie']});
 *
 * How would this be done without PropertySet (for comparison)?
 * //Normally would be created in a class but that is omitted here for brevity.
 * var p ={name: new Property('larry'), age: new Property('age'), kids: new Property(['alice','bob'])}
 * p.reset = function(){
 *   this.name.reset();
 *   this.age.reset();
 *   this.kids.reset();
 * }
 * p.name.set('clark');
 * p.age.set('102');
 * p.kids.set(['alice','bob','charlie']);
 *
 * @author Sam Reid
 */

define( function( require ) {
  'use strict';

  var Property = require( 'AXON/Property' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var axon = require( 'AXON/axon' );

  /**
   * @class PropertySet
   * @constructor
   * @param values an object hash with the initial values for the properties
   */
  axon.PropertySet = function PropertySet( values ) {
    var propertySet = this;

    //Keep track of the keys so we know which to reset
    this.keys = [];

    Object.getOwnPropertyNames( values ).forEach( function( value ) {
      propertySet.addProperty( value, values[value] );
    } );

    this.eventListeners = {};
  };

  axon.PropertySet.prototype = {

    /**
     * Adds a new property to this PropertySet
     *
     * @param {String} name
     * @param value
     */
    addProperty: function( name, value ) {
      this[name + 'Property'] = new Property( value );
      this.addGetterAndSetter( name );
      this.keys.push( name );
    },

    /**
     * Remove any property (whether a derived property or not) that was added to this PropertySet
     * @param name
     */
    removeProperty: function( name ) {

      //Remove from the keys (only for non-derived properties)
      var index = this.keys.indexOf( name );
      if ( index !== -1 ) {
        this.keys.splice( index, index + 1 );
      }

      //Unregister the Property instance from the PropertySet
      delete this[name + 'Property'];

      //Unregister the getter/setter, if they exist
      delete this[name];
    },

    //Taken from https://gist.github.com/dandean/1292057, same as in github/Atlas
    addGetterAndSetter: function( name ) {
      var propertyName = name + 'Property';

      //TODO: Store the this[propertyName] in a closure for performance?  Memory/performance tradeoff, and problems if the property instance ever changes (unlikely)
      //TODO: If a subclass ever substitutes a property like this: person.ageProperty = new Property(person.age), then it would break the getter/setter
      Object.defineProperty( this, name, {

        // Getter proxies to Model#get()...
        get: function() { return this[propertyName].get();},

        // Setter proxies to Model#set(attributes)
        set: function( value ) { this[propertyName].set( value );},

        // Make it configurable and enumerable so it's easy to override...
        configurable: true,
        enumerable: true
      } );
    },

    addGetter: function( name ) {

      //TODO: Store the this[propertyName] for performance?
      var propertyName = name + 'Property';
      Object.defineProperty( this, name, {

        get: function() { return this[propertyName].get();},

        // Make it configurable and enumerable so it's easy to override...
        configurable: true,
        enumerable: true
      } );
    },

    //Resets all of the properties associated with this PropertySet
    reset: function() {
      var propertySet = this;
      this.keys.forEach( function( key ) {
        propertySet[key + 'Property'].reset();
      } );
    },

    /**
     * Creates a DerivedProperty from the given dependency names and derivation.
     * @param dependencyNames {Array<String>}
     * @param derivation {Function}
     * @returns {DerivedProperty}
     */
    toDerivedProperty: function( dependencyNames, derivation ) {
      var propertySet = this;
      var dependencies = dependencyNames.map( function( dependency ) {
        return propertySet[dependency + 'Property'];
      } );
      return new DerivedProperty( dependencies, derivation );
    },

    addDerivedProperty: function( name, dependencyNames, derivation ) {
      this[name + 'Property'] = this.toDerivedProperty( dependencyNames, derivation );
      this.addGetter( name );
    },

    /**
     * Set all of the values specified in the object hash
     * Allows you to use this form:
     * puller.set( {x: knot.x, y: knot.y, knot: knot} );
     *
     * instead of this:
     * puller.x.value = knot.x;
     * puller.y.value = knot.y;
     * puller.knot.value = knot;
     *
     * Throws an error if you try to set a value for which there is no property.
     */
    set: function( values ) {
      var propertySet = this;
      Object.getOwnPropertyNames( values ).forEach( function( val ) {
        if ( typeof(propertySet[val + 'Property'] === 'Property') ) {
          propertySet[val + 'Property'].set( values[val] );
        }
        else {
          throw new Error( 'property not found: ' + val );
        }
      } );
    },

    /**
     * Add a listener to zero or more properties in this PropertySet, useful when you have an update function
     * that relies on several properties.  Similar to DerivedProperty.
     * Discussion result: Let's use 'multilink' for now, and in the future we may change it to link.
     * @param dependencyNames {Array<String>} the list of dependencies to use
     * @param listener {Function} the listener to call back, with signature matching the dependency names
     */
    multilink: function( dependencyNames, listener ) {
      return this.toDerivedProperty( dependencyNames, listener );
    },

    /**
     * Removes the multilinked listener from this PropertySet.
     * Same as calling detach() on the handle (which happens to be a DerivedProperty instance)
     * @param derivedProperty
     */
    unmultilink: function( derivedProperty ) {
      derivedProperty.detach();
    },

    toString: function() {
      var text = 'PropertySet{';
      var propertySet = this;
      for ( var i = 0; i < this.keys.length; i++ ) {
        var key = this.keys[i];
        text = text + key + ':' + propertySet[key].toString();
        if ( i < this.keys.length - 1 ) {
          text = text + ',';
        }
      }
      return text + '}';
    },

    /////////////////////////////////////////////
    // Below this point are the functions for event handling, basically orthogonal to property value change notifications

    /**
     * Register a listener when the specified eventName is triggered.
     * @param eventName {String} the name for the event channel
     * @param callback
     */
    on: function( eventName, callback ) {
      this.eventListeners[eventName] = this.eventListeners[eventName] || [];
      this.eventListeners[eventName].push( callback );
    },

    /**
     * Adds a function which will only be called back once, after which it is removed as a listener.
     * If you need to remove a function added with 'once' you will have to remove its handle, which is returned by the function.
     * @param eventName {String} the name for the event channel
     * @param callback function to be called back once (if at all)
     */
    once: function( eventName, callback ) {
      var propertySet = this;
      var wrappedCallback = function() {
        propertySet.off( eventName, wrappedCallback );

        //If no arguments being passed through, call back without processing arguments, for possible speed
        if ( arguments.length === 0 ) {
          callback();
        }
        else {

          //General case of passing events through to the wrapped callback function
          callback.apply( this, Array.prototype.slice.call( arguments, 0 ) );
        }
      };
      this.on( eventName, wrappedCallback );

      //Return the handle in case it needs to be removed.
      return wrappedCallback;
    },

    /**
     * Remove a listener from the specified event type.  Does nothing if the listener did not exist
     * @param eventName {String} the name for the event channel
     * @param callback
     */
    off: function( eventName, callback ) {
      if ( this.eventListeners[eventName] ) {
        var index = this.eventListeners[eventName].indexOf( callback );
        if ( index !== -1 ) {
          this.eventListeners[eventName].splice( index, index + 1 );
        }
      }
    },

    /**
     * Trigger an event with the specified name and arguments.
     * @param eventName {String} the name for the event channel
     * @param args... optional arguments to pass to the listeners
     */
    trigger: function( eventName ) {
      if ( this.eventListeners[eventName] ) {
        var listenersCopy = this.eventListeners[eventName].slice(); // make a copy, in case callback removes listener
        for ( var i = 0; i < listenersCopy.length; i++ ) {
          var listener = listenersCopy[i];

          //Simple case of no arguments, call it separately for improved performance in case it is faster (untested)
          if ( arguments.length === 1 ) {
            listener( arguments );
          }
          else {
            var suffix = Array.prototype.slice.call( arguments, 1 );
            listener.apply( this, suffix );
          }
        }
      }
    }
  };

  return axon.PropertySet;
} );<commit_msg>Use properties in closures to improve callback performance, see #8<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * PropertySet facilitates creation and use of multiple named Property instances.  There are still several API design issues in question, but this
 * class is ready for use.
 *
 * A PropertySet is a set of Property instances that provides support for:
 * -Easily creating several properties using an object literal (hash)
 * -Resetting them as a group
 * -Set multiple values at once, using propertySet.set({x:100,y:200,name:'alice'});
 * -Support for derived properties, which appear with the same interface as basic properties
 * -Convenient toString that prints e.g., PropertySet{name:'larry',age:101,kids:['alice','bob']}
 * -Wiring up to listen to multiple properties simultaneously
 * -Add properties after the PropertySet is created?  Don't forget to add to the key list as well.
 * -Remove properties that were added using addProperty or the constructor
 * -TODO: Make it easy to mix-in with model classes?  Subclassing PropertySet already works fairly well, so this may good enough already.
 *
 * Sample usage:
 * var p = new PropertySet( {name: 'larry', age: 100, kids: ['alice', 'bob']} );
 * p.nameProperty.link( function( n ) {console.log( 'hello ' + n );} );
 * p.name = 'jensen';
 * p.age = 101;//Happy Birthday!
 * console.log( p );
 * p.reset();
 * console.log( p );
 * p.set({name:'clark',age:102,kids:['alice','bob','charlie']});
 *
 * How would this be done without PropertySet (for comparison)?
 * //Normally would be created in a class but that is omitted here for brevity.
 * var p ={name: new Property('larry'), age: new Property('age'), kids: new Property(['alice','bob'])}
 * p.reset = function(){
 *   this.name.reset();
 *   this.age.reset();
 *   this.kids.reset();
 * }
 * p.name.set('clark');
 * p.age.set('102');
 * p.kids.set(['alice','bob','charlie']);
 *
 * Note: If a subclass ever substitutes a property like this: person.ageProperty = new Property(person.age), then it would break the getter/setter
 * @author Sam Reid
 */

define( function( require ) {
  'use strict';

  var Property = require( 'AXON/Property' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var axon = require( 'AXON/axon' );

  /**
   * @class PropertySet
   * @constructor
   * @param values an object hash with the initial values for the properties
   */
  axon.PropertySet = function PropertySet( values ) {
    var propertySet = this;

    //Keep track of the keys so we know which to reset
    this.keys = [];

    Object.getOwnPropertyNames( values ).forEach( function( value ) {
      propertySet.addProperty( value, values[value] );
    } );

    this.eventListeners = {};
  };

  axon.PropertySet.prototype = {

    /**
     * Adds a new property to this PropertySet
     *
     * @param {String} name
     * @param value
     */
    addProperty: function( name, value ) {
      this[name + 'Property'] = new Property( value );
      this.addGetterAndSetter( name );
      this.keys.push( name );
    },

    /**
     * Remove any property (whether a derived property or not) that was added to this PropertySet
     * @param name
     */
    removeProperty: function( name ) {

      //Remove from the keys (only for non-derived properties)
      var index = this.keys.indexOf( name );
      if ( index !== -1 ) {
        this.keys.splice( index, index + 1 );
      }

      //Unregister the Property instance from the PropertySet
      delete this[name + 'Property'];

      //Unregister the getter/setter, if they exist
      delete this[name];
    },

    //Add a getter and setter using ES5 get/set syntax, similar to https://gist.github.com/dandean/1292057, same as in github/Atlas
    addGetterAndSetter: function( name ) {
      var property = this[name + 'Property'];

      Object.defineProperty( this, name, {

        // Getter proxies to Model#get()...
        get: function() { return property.get();},

        // Setter proxies to Model#set(attributes)
        set: function( value ) { property.set( value );},

        // Make it configurable and enumerable so it's easy to override...
        configurable: true,
        enumerable: true
      } );
    },

    addGetter: function( name ) {
      var property = this[name + 'Property'];

      Object.defineProperty( this, name, {

        get: function() { return property.get();},

        // Make it configurable and enumerable so it's easy to override...
        configurable: true,
        enumerable: true
      } );
    },

    //Resets all of the properties associated with this PropertySet
    reset: function() {
      var propertySet = this;
      this.keys.forEach( function( key ) {
        propertySet[key + 'Property'].reset();
      } );
    },

    /**
     * Creates a DerivedProperty from the given dependency names and derivation.
     * @param dependencyNames {Array<String>}
     * @param derivation {Function}
     * @returns {DerivedProperty}
     */
    toDerivedProperty: function( dependencyNames, derivation ) {
      var propertySet = this;
      var dependencies = dependencyNames.map( function( dependency ) {
        return propertySet[dependency + 'Property'];
      } );
      return new DerivedProperty( dependencies, derivation );
    },

    addDerivedProperty: function( name, dependencyNames, derivation ) {
      this[name + 'Property'] = this.toDerivedProperty( dependencyNames, derivation );
      this.addGetter( name );
    },

    /**
     * Set all of the values specified in the object hash
     * Allows you to use this form:
     * puller.set( {x: knot.x, y: knot.y, knot: knot} );
     *
     * instead of this:
     * puller.x.value = knot.x;
     * puller.y.value = knot.y;
     * puller.knot.value = knot;
     *
     * Throws an error if you try to set a value for which there is no property.
     */
    set: function( values ) {
      var propertySet = this;
      Object.getOwnPropertyNames( values ).forEach( function( val ) {
        if ( typeof(propertySet[val + 'Property'] === 'Property') ) {
          propertySet[val + 'Property'].set( values[val] );
        }
        else {
          throw new Error( 'property not found: ' + val );
        }
      } );
    },

    /**
     * Add a listener to zero or more properties in this PropertySet, useful when you have an update function
     * that relies on several properties.  Similar to DerivedProperty.
     * Discussion result: Let's use 'multilink' for now, and in the future we may change it to link.
     * @param dependencyNames {Array<String>} the list of dependencies to use
     * @param listener {Function} the listener to call back, with signature matching the dependency names
     */
    multilink: function( dependencyNames, listener ) {
      return this.toDerivedProperty( dependencyNames, listener );
    },

    /**
     * Removes the multilinked listener from this PropertySet.
     * Same as calling detach() on the handle (which happens to be a DerivedProperty instance)
     * @param derivedProperty
     */
    unmultilink: function( derivedProperty ) {
      derivedProperty.detach();
    },

    toString: function() {
      var text = 'PropertySet{';
      var propertySet = this;
      for ( var i = 0; i < this.keys.length; i++ ) {
        var key = this.keys[i];
        text = text + key + ':' + propertySet[key].toString();
        if ( i < this.keys.length - 1 ) {
          text = text + ',';
        }
      }
      return text + '}';
    },

    /////////////////////////////////////////////
    // Below this point are the functions for event handling, basically orthogonal to property value change notifications

    /**
     * Register a listener when the specified eventName is triggered.
     * @param eventName {String} the name for the event channel
     * @param callback
     */
    on: function( eventName, callback ) {
      this.eventListeners[eventName] = this.eventListeners[eventName] || [];
      this.eventListeners[eventName].push( callback );
    },

    /**
     * Adds a function which will only be called back once, after which it is removed as a listener.
     * If you need to remove a function added with 'once' you will have to remove its handle, which is returned by the function.
     * @param eventName {String} the name for the event channel
     * @param callback function to be called back once (if at all)
     */
    once: function( eventName, callback ) {
      var propertySet = this;
      var wrappedCallback = function() {
        propertySet.off( eventName, wrappedCallback );

        //If no arguments being passed through, call back without processing arguments, for possible speed
        if ( arguments.length === 0 ) {
          callback();
        }
        else {

          //General case of passing events through to the wrapped callback function
          callback.apply( this, Array.prototype.slice.call( arguments, 0 ) );
        }
      };
      this.on( eventName, wrappedCallback );

      //Return the handle in case it needs to be removed.
      return wrappedCallback;
    },

    /**
     * Remove a listener from the specified event type.  Does nothing if the listener did not exist
     * @param eventName {String} the name for the event channel
     * @param callback
     */
    off: function( eventName, callback ) {
      if ( this.eventListeners[eventName] ) {
        var index = this.eventListeners[eventName].indexOf( callback );
        if ( index !== -1 ) {
          this.eventListeners[eventName].splice( index, index + 1 );
        }
      }
    },

    /**
     * Trigger an event with the specified name and arguments.
     * @param eventName {String} the name for the event channel
     * @param args... optional arguments to pass to the listeners
     */
    trigger: function( eventName ) {
      if ( this.eventListeners[eventName] ) {
        var listenersCopy = this.eventListeners[eventName].slice(); // make a copy, in case callback removes listener
        for ( var i = 0; i < listenersCopy.length; i++ ) {
          var listener = listenersCopy[i];

          //Simple case of no arguments, call it separately for improved performance in case it is faster (untested)
          if ( arguments.length === 1 ) {
            listener( arguments );
          }
          else {
            var suffix = Array.prototype.slice.call( arguments, 1 );
            listener.apply( this, suffix );
          }
        }
      }
    }
  };

  return axon.PropertySet;
} );<|endoftext|>


<commit_before>const { URL } = require('url') ;

var adapterFor = (function() {
    var adapters = {
        'http:': require('http'),
        'https:': require('https'),
    };

    return function(protocol) {
        return adapters[protocol];
    };
}());

var PrestoHeaders = require('./headers').Headers;
var TrinoHeaders = require('./headers').TrinoHeaders;

var QUERY_STATE_CHECK_INTERVAL = 800; // 800ms

exports.version = 'unknown';

var Client = exports.Client = function(args){
    if (!args)
        args = {};

    switch (args.engine) {
        case 'trino':
            this.headers = TrinoHeaders;
            break;
        case 'presto':
        default:
            this.headers = PrestoHeaders;
            break;
    }

    // exports.version set in index.js of project root
    this.userAgent = 'presto-client-node ' + exports.version;

    this.host = args.host || 'localhost';
    this.port = args.port || 8080;
    this.user = args.user || process.env.USER;

    // HTTP Authorization header
    if (args.custom_auth && args.basic_auth)
        throw new Error('Please do not specify basic_auth and custom_auth at the same time.');

    if (args.custom_auth) {
        this.authorization = args.custom_auth;
    } else if (args.basic_auth) {
        this.authorization = 'Basic ' + new Buffer(args.basic_auth.user + ':' + args.basic_auth.password).toString('base64');
    }

    this.protocol = 'http:';

    this.catalog = args.catalog;
    this.schema = args.schema;

    this.source = args.source || 'nodejs-client';

    this.checkInterval = args.checkInterval || QUERY_STATE_CHECK_INTERVAL;
    this.enableVerboseStateCallback = args.enableVerboseStateCallback || false;
    this.jsonParser = args.jsonParser || JSON;

    if (args.ssl) {
        this.protocol = 'https:';
        this.ssl = args.ssl;
    }
};

Client.prototype.request = function(opts, callback) {
    var client = this;
    var adapter = adapterFor(client.protocol);
    var contentBody = null;

    if (opts instanceof Object) {
        opts.host = client.host;
        opts.port = client.port;
        if (! opts.user)
            opts.user = client.user
        if (! opts.headers)
            opts.headers = {};
    } else {
        try {
            // "opts" argument should be an URL, probably nextUri
            var href = new URL(opts);
            opts = {
                method: 'GET',
                host: href.hostname,
                port: href.port || (href.protocol === 'https:' ? '443' : '80'),
                path: href.pathname + href.search,
                headers: {},
            };
        } catch (error) {
            return callback(error);
        }
    }
    opts.protocol = client.protocol;

    opts = Object.assign({}, opts, client.ssl);

    if (opts.user)
        opts.headers[client.headers.USER] = opts.user;

    if (client.source)
        opts.headers[client.headers.SOURCE] = client.source;

    opts.headers[client.headers.USER_AGENT] = this.userAgent;

    if (client.authorization)
        opts.headers[client.headers.AUTHORIZATION] = this.authorization;

    if (opts.body)
        contentBody = opts.body;

    opts.agent = new adapter.Agent(opts); // Otherwise SSL params are ignored.

    var parser = this.jsonParser;

    var req = adapter.request(opts, function(res){
        var response_code = res.statusCode;
        var response_data = '';
        res.setEncoding('utf8');
        res.on('data', function(chunk){
            response_data += chunk;
        });
        res.on('end', function(){
            var data = response_data;
            if (response_code < 300 && (data[0] === '{' || data[0] === '[')) {
                try { data = parser.parse(data); }
                catch (x) {
                    /* ignore json parse error (and don't parse) for non-json content body */
                }
            }
            callback(null, response_code, data);
        });
    });

    req.on('error', function(e){
        callback(e);
    });

    if (contentBody)
        req.write(contentBody);

    req.end();
};

Client.prototype.nodes = function(opts, callback) { // TODO: "failed" nodes not supported yet
    if (! callback) {
        callback = opts;
        opts = {};
    }

    this.request({ method: 'GET', path: '/v1/node' }, function(error, code, data){
        if (error || code !== 200) {
            var message = "node list api returns error" + (data && data.length > 0 ? ":" + data : "");
            callback({message: message, error: error, code: code});
            return;
        }
        callback(null, data);
    });
};

Client.prototype.query = function(query_id, callback) {
    this.request({ method: 'GET', path: '/v1/query/' + query_id }, function(error, code, data){
        if (error || code !== 200) {
            var message = "query info api returns error" + (data && data.length > 0 ? ":" + data : "");
            callback({message: message, error: error, code: code});
            return;
        }
        callback(null, data);
    });
};

Client.prototype.kill = function(query_id, callback) {
    this.request({ method: 'DELETE', path: '/v1/query/' + query_id }, function(error, code, data){
        if (error || code !== 204) {
            var message = "query kill api returns error" + (data && data.length > 0 ? ":" + data : "");
            callback({message: message, error: error, code: code});
            return;
        }
        if (callback)
            callback(null);
    });
};

Client.prototype.execute = function(opts) {
    this.statementResource(opts);
};

Client.prototype.statementResource = function(opts) {
    var client = this;
    var columns = null;

    if ((opts.schema || this.schema) && !(opts.catalog || this.catalog)) {
        throw {message: "Catalog not specified; catalog is required if schema is specified"}
    }
    if (!opts.success && !opts.callback)
        throw {message: "callback function 'success' (or 'callback') not specified"};

    var header = {};
    if (opts.catalog || this.catalog) {
        header[client.headers.CATALOG] = opts.catalog || this.catalog;
    }
    if (opts.schema || this.schema) {
        header[client.headers.SCHEMA] = opts.schema || this.schema;
    }
    if (opts.prepares) {
        header[client.headers.PREPARE] = opts.prepares.map((s, index) => 'query' + index + '=' + urlencode(s)).join(',');
    }

    if (opts.session)
        header[client.headers.SESSION] = opts.session;
    if (opts.timezone)
        header[client.headers.TIME_ZONE] = opts.timezone;

    var fetch_info = opts.info || false;

    var cancel_checker = opts.cancel;
    var state_callback = opts.state;
    var columns_callback = opts.columns;
    var data_callback = opts.data;
    var success_callback = opts.success || opts.callback;
    var error_callback = opts.error || opts.callback;

    var enable_verbose_state_callback = this.enableVerboseStateCallback || false;

    var req = { method: 'POST', path: '/v1/statement', headers: header, body: opts.query, user: opts.user };
    client.request(req, function(err, code, data){
        if (err || code !== 200 || (data && data.error)) {
            if (error_callback) {
                var message = "execution error" + (data && data.length > 0 ? ":" + data : "");
                if (data && data.error && data.error.message)
                    message = data.error.message;
                error_callback({message:message, error: (err || data.error), code: code});
            }
            return;
        }
        /*
          var data = {
              "stats": {
                  "processedBytes": 0,
                  "processedRows": 0,
                  "wallTimeMillis": 0,
                  "cpuTimeMillis": 0,
                  "userTimeMillis": 0,
                  "state": "QUEUED",
                  "scheduled": false,
                  "nodes": 0,
                  "totalSplits": 0,
                  "queuedSplits": 0,
                  "runningSplits": 0,
                  "completedSplits": 0,
              },
              "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/1",
              "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
              "id": "20140120_032523_00000_32v8g"
          };
        */
        if (!data.id || !data.nextUri || !data.infoUri) {
            var error_message = null;
            if (!data.id)
                error_message = "query id missing in response for POST /v1/statement";
            else if (!data.nextUri)
                error_message = "nextUri missing in response for POST /v1/statement";
            else if (!data.infoUri)
                error_message = "infoUri missing in response for POST /v1/statement";
            error_callback({message: error_message, data: data});
            return;
        }
        var last_state = null;
        var firstNextUri = data.nextUri; // TODO: check the cases without nextUri for /statement ?
        var fetch_next = function(next_uri){
            /*
             * 1st time
             {
                 "stats": {
                     "rootStage": {
                         "subStages": [
                             {
                                 "subStages": [],
                                 "processedBytes": 83103149,
                                 "processedRows": 2532704,
                                 "wallTimeMillis": 20502,
                                 "cpuTimeMillis": 3431,
                                 "userTimeMillis": 3210,
                                 "stageId": "1",
                                 "state": "FINISHED",
                                 "done": true,
                                 "nodes": 3,
                                 "totalSplits": 420,
                                 "queuedSplits": 0,
                                 "runningSplits": 0,
                                 "completedSplits": 420
                             }
                         ],
                         // same as substage
                     },
                     // same as substage
                     "state": "RUNNING",
                 },
                 "data": [ [ 1266352 ] ],
                 "columns": [ { "type": "bigint", "name": "cnt" } ],
                 "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/2",
                 "partialCancelUri": "http://10.0.0.0:8080/v1/stage/20140120_032523_00000_32v8g.0",
                 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
                 "id": "20140120_032523_00000_32v8g"
             }
            */
            /*
             * 2nd time
             {
                 "stats": {
                     // ....
                     "state": "FINISHED",
                 },
                 "columns": [ { "type": "bigint", "name": "cnt" } ],
                 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
                 "id": "20140120_032523_00000_32v8g"
             }
            */
            if (cancel_checker && cancel_checker()) {
                client.request({ method: 'DELETE', path: next_uri }, function(error, code, data){
                    if (error || code !== 204) {
                        error_callback({message: "query fetch canceled, but Presto query cancel may fail", error: error, code: code});
                    } else {
                        error_callback({message: "query fetch canceled by operation"});
                    }
                });
                return;
            }
            client.request(next_uri, function(error, code, response){
                if (error || response.error) {
                    error_callback(error || response.error);
                    return;
                }

                if (state_callback && (last_state !== response.stats.state || enable_verbose_state_callback)) {
                    state_callback(null, response.id, response.stats);
                    last_state = response.stats.state;
                }

                if (columns_callback && response.columns && !columns) {
                    columns = response.columns;
                    columns_callback(null, columns);
                }

                var fetchNextWithTimeout = function(uri, checkInterval) {
                    setTimeout(function(){ fetch_next(uri); }, checkInterval);
                };

                /* presto-main/src/main/java/com/facebook/presto/execution/QueryState.java
                 * QUEUED, PLANNING, STARTING, RUNNING, FINISHED, CANCELED, FAILED
                 */
                if (response.stats.state === 'QUEUED'
                    || response.stats.state === 'PLANNING'
                    || response.stats.state === 'STARTING'
                    || response.stats.state === 'RUNNING' && !response.data) {
                    fetchNextWithTimeout(response.nextUri, client.checkInterval);
                    return;
                }

                if (data_callback && response.data) {
                    data_callback(null, response.data, response.columns, response.stats);
                }

                if (response.nextUri) {
                    fetchNextWithTimeout(response.nextUri, client.checkInterval);
                    return;
                }

                var finishedStats = response.stats;

                if (fetch_info && response.infoUri) {
                    client.request(response.infoUri, function(error, code, response){
                        success_callback(null, finishedStats, response);
                    });
                }
                else {
                    success_callback(null, finishedStats);
                }
            });
        };
        fetch_next(firstNextUri);
    });
};
<commit_msg>Fix "urlencode is not defined"<commit_after>const { URL } = require('url') ;

var adapterFor = (function() {
    var adapters = {
        'http:': require('http'),
        'https:': require('https'),
    };

    return function(protocol) {
        return adapters[protocol];
    };
}());

var PrestoHeaders = require('./headers').Headers;
var TrinoHeaders = require('./headers').TrinoHeaders;

var QUERY_STATE_CHECK_INTERVAL = 800; // 800ms

exports.version = 'unknown';

var Client = exports.Client = function(args){
    if (!args)
        args = {};

    switch (args.engine) {
        case 'trino':
            this.headers = TrinoHeaders;
            break;
        case 'presto':
        default:
            this.headers = PrestoHeaders;
            break;
    }

    // exports.version set in index.js of project root
    this.userAgent = 'presto-client-node ' + exports.version;

    this.host = args.host || 'localhost';
    this.port = args.port || 8080;
    this.user = args.user || process.env.USER;

    // HTTP Authorization header
    if (args.custom_auth && args.basic_auth)
        throw new Error('Please do not specify basic_auth and custom_auth at the same time.');

    if (args.custom_auth) {
        this.authorization = args.custom_auth;
    } else if (args.basic_auth) {
        this.authorization = 'Basic ' + new Buffer(args.basic_auth.user + ':' + args.basic_auth.password).toString('base64');
    }

    this.protocol = 'http:';

    this.catalog = args.catalog;
    this.schema = args.schema;

    this.source = args.source || 'nodejs-client';

    this.checkInterval = args.checkInterval || QUERY_STATE_CHECK_INTERVAL;
    this.enableVerboseStateCallback = args.enableVerboseStateCallback || false;
    this.jsonParser = args.jsonParser || JSON;

    if (args.ssl) {
        this.protocol = 'https:';
        this.ssl = args.ssl;
    }
};

Client.prototype.request = function(opts, callback) {
    var client = this;
    var adapter = adapterFor(client.protocol);
    var contentBody = null;

    if (opts instanceof Object) {
        opts.host = client.host;
        opts.port = client.port;
        if (! opts.user)
            opts.user = client.user
        if (! opts.headers)
            opts.headers = {};
    } else {
        try {
            // "opts" argument should be an URL, probably nextUri
            var href = new URL(opts);
            opts = {
                method: 'GET',
                host: href.hostname,
                port: href.port || (href.protocol === 'https:' ? '443' : '80'),
                path: href.pathname + href.search,
                headers: {},
            };
        } catch (error) {
            return callback(error);
        }
    }
    opts.protocol = client.protocol;

    opts = Object.assign({}, opts, client.ssl);

    if (opts.user)
        opts.headers[client.headers.USER] = opts.user;

    if (client.source)
        opts.headers[client.headers.SOURCE] = client.source;

    opts.headers[client.headers.USER_AGENT] = this.userAgent;

    if (client.authorization)
        opts.headers[client.headers.AUTHORIZATION] = this.authorization;

    if (opts.body)
        contentBody = opts.body;

    opts.agent = new adapter.Agent(opts); // Otherwise SSL params are ignored.

    var parser = this.jsonParser;

    var req = adapter.request(opts, function(res){
        var response_code = res.statusCode;
        var response_data = '';
        res.setEncoding('utf8');
        res.on('data', function(chunk){
            response_data += chunk;
        });
        res.on('end', function(){
            var data = response_data;
            if (response_code < 300 && (data[0] === '{' || data[0] === '[')) {
                try { data = parser.parse(data); }
                catch (x) {
                    /* ignore json parse error (and don't parse) for non-json content body */
                }
            }
            callback(null, response_code, data);
        });
    });

    req.on('error', function(e){
        callback(e);
    });

    if (contentBody)
        req.write(contentBody);

    req.end();
};

Client.prototype.nodes = function(opts, callback) { // TODO: "failed" nodes not supported yet
    if (! callback) {
        callback = opts;
        opts = {};
    }

    this.request({ method: 'GET', path: '/v1/node' }, function(error, code, data){
        if (error || code !== 200) {
            var message = "node list api returns error" + (data && data.length > 0 ? ":" + data : "");
            callback({message: message, error: error, code: code});
            return;
        }
        callback(null, data);
    });
};

Client.prototype.query = function(query_id, callback) {
    this.request({ method: 'GET', path: '/v1/query/' + query_id }, function(error, code, data){
        if (error || code !== 200) {
            var message = "query info api returns error" + (data && data.length > 0 ? ":" + data : "");
            callback({message: message, error: error, code: code});
            return;
        }
        callback(null, data);
    });
};

Client.prototype.kill = function(query_id, callback) {
    this.request({ method: 'DELETE', path: '/v1/query/' + query_id }, function(error, code, data){
        if (error || code !== 204) {
            var message = "query kill api returns error" + (data && data.length > 0 ? ":" + data : "");
            callback({message: message, error: error, code: code});
            return;
        }
        if (callback)
            callback(null);
    });
};

Client.prototype.execute = function(opts) {
    this.statementResource(opts);
};

Client.prototype.statementResource = function(opts) {
    var client = this;
    var columns = null;

    if ((opts.schema || this.schema) && !(opts.catalog || this.catalog)) {
        throw {message: "Catalog not specified; catalog is required if schema is specified"}
    }
    if (!opts.success && !opts.callback)
        throw {message: "callback function 'success' (or 'callback') not specified"};

    var header = {};
    if (opts.catalog || this.catalog) {
        header[client.headers.CATALOG] = opts.catalog || this.catalog;
    }
    if (opts.schema || this.schema) {
        header[client.headers.SCHEMA] = opts.schema || this.schema;
    }
    if (opts.prepares) {
        header[client.headers.PREPARE] = opts.prepares.map((s, index) => 'query' + index + '=' + encodeURIComponent(s)).join(',');
    }

    if (opts.session)
        header[client.headers.SESSION] = opts.session;
    if (opts.timezone)
        header[client.headers.TIME_ZONE] = opts.timezone;

    var fetch_info = opts.info || false;

    var cancel_checker = opts.cancel;
    var state_callback = opts.state;
    var columns_callback = opts.columns;
    var data_callback = opts.data;
    var success_callback = opts.success || opts.callback;
    var error_callback = opts.error || opts.callback;

    var enable_verbose_state_callback = this.enableVerboseStateCallback || false;

    var req = { method: 'POST', path: '/v1/statement', headers: header, body: opts.query, user: opts.user };
    client.request(req, function(err, code, data){
        if (err || code !== 200 || (data && data.error)) {
            if (error_callback) {
                var message = "execution error" + (data && data.length > 0 ? ":" + data : "");
                if (data && data.error && data.error.message)
                    message = data.error.message;
                error_callback({message:message, error: (err || data.error), code: code});
            }
            return;
        }
        /*
          var data = {
              "stats": {
                  "processedBytes": 0,
                  "processedRows": 0,
                  "wallTimeMillis": 0,
                  "cpuTimeMillis": 0,
                  "userTimeMillis": 0,
                  "state": "QUEUED",
                  "scheduled": false,
                  "nodes": 0,
                  "totalSplits": 0,
                  "queuedSplits": 0,
                  "runningSplits": 0,
                  "completedSplits": 0,
              },
              "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/1",
              "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
              "id": "20140120_032523_00000_32v8g"
          };
        */
        if (!data.id || !data.nextUri || !data.infoUri) {
            var error_message = null;
            if (!data.id)
                error_message = "query id missing in response for POST /v1/statement";
            else if (!data.nextUri)
                error_message = "nextUri missing in response for POST /v1/statement";
            else if (!data.infoUri)
                error_message = "infoUri missing in response for POST /v1/statement";
            error_callback({message: error_message, data: data});
            return;
        }
        var last_state = null;
        var firstNextUri = data.nextUri; // TODO: check the cases without nextUri for /statement ?
        var fetch_next = function(next_uri){
            /*
             * 1st time
             {
                 "stats": {
                     "rootStage": {
                         "subStages": [
                             {
                                 "subStages": [],
                                 "processedBytes": 83103149,
                                 "processedRows": 2532704,
                                 "wallTimeMillis": 20502,
                                 "cpuTimeMillis": 3431,
                                 "userTimeMillis": 3210,
                                 "stageId": "1",
                                 "state": "FINISHED",
                                 "done": true,
                                 "nodes": 3,
                                 "totalSplits": 420,
                                 "queuedSplits": 0,
                                 "runningSplits": 0,
                                 "completedSplits": 420
                             }
                         ],
                         // same as substage
                     },
                     // same as substage
                     "state": "RUNNING",
                 },
                 "data": [ [ 1266352 ] ],
                 "columns": [ { "type": "bigint", "name": "cnt" } ],
                 "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/2",
                 "partialCancelUri": "http://10.0.0.0:8080/v1/stage/20140120_032523_00000_32v8g.0",
                 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
                 "id": "20140120_032523_00000_32v8g"
             }
            */
            /*
             * 2nd time
             {
                 "stats": {
                     // ....
                     "state": "FINISHED",
                 },
                 "columns": [ { "type": "bigint", "name": "cnt" } ],
                 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
                 "id": "20140120_032523_00000_32v8g"
             }
            */
            if (cancel_checker && cancel_checker()) {
                client.request({ method: 'DELETE', path: next_uri }, function(error, code, data){
                    if (error || code !== 204) {
                        error_callback({message: "query fetch canceled, but Presto query cancel may fail", error: error, code: code});
                    } else {
                        error_callback({message: "query fetch canceled by operation"});
                    }
                });
                return;
            }
            client.request(next_uri, function(error, code, response){
                if (error || response.error) {
                    error_callback(error || response.error);
                    return;
                }

                if (state_callback && (last_state !== response.stats.state || enable_verbose_state_callback)) {
                    state_callback(null, response.id, response.stats);
                    last_state = response.stats.state;
                }

                if (columns_callback && response.columns && !columns) {
                    columns = response.columns;
                    columns_callback(null, columns);
                }

                var fetchNextWithTimeout = function(uri, checkInterval) {
                    setTimeout(function(){ fetch_next(uri); }, checkInterval);
                };

                /* presto-main/src/main/java/com/facebook/presto/execution/QueryState.java
                 * QUEUED, PLANNING, STARTING, RUNNING, FINISHED, CANCELED, FAILED
                 */
                if (response.stats.state === 'QUEUED'
                    || response.stats.state === 'PLANNING'
                    || response.stats.state === 'STARTING'
                    || response.stats.state === 'RUNNING' && !response.data) {
                    fetchNextWithTimeout(response.nextUri, client.checkInterval);
                    return;
                }

                if (data_callback && response.data) {
                    data_callback(null, response.data, response.columns, response.stats);
                }

                if (response.nextUri) {
                    fetchNextWithTimeout(response.nextUri, client.checkInterval);
                    return;
                }

                var finishedStats = response.stats;

                if (fetch_info && response.infoUri) {
                    client.request(response.infoUri, function(error, code, response){
                        success_callback(null, finishedStats, response);
                    });
                }
                else {
                    success_callback(null, finishedStats);
                }
            });
        };
        fetch_next(firstNextUri);
    });
};
<|endoftext|>


<commit_before>var splReport = {

	api: {
		url: '//staff.spokanelibrary.org/wordpress/wp-admin/admin-ajax.php'
		,data: { action: 'spl_reports'
						,nonce: $('.spl-report').data('spl-report-nonce')
						,params: {
							ajax: true
							,id: $('.spl-report').data('spl-report-id')
					}
		}
		,type: 'POST'
		,dataType: 'json'
	}

	,init: function() {
		_this = this;
		this.initUI();
	}
	,initUI: function() {
		$('body').on('submit', '.spl-report-control', function(e) {
			if ( $(this).data('noajax') ) {
			} else {
				e.preventDefault();
				_this.getReport();
			}
		});

		if ( $('#spl-report-json').length > 0 ) {
      var $report = $('#spl-report-json');
      //if ( $account && $account.text().length > 0 ) {
      if ( $report && $report.html().length > 0 ) {
        //var user = JSON.parse($account.text());
        var report = JSON.parse($report.html());
      	console.log(report);
        _this.showReport(report);
      }
    }

		$datepicker = $('.date');
		if ( $datepicker.length ) {
			$datepicker.datepicker({
	        //startDate: "+1d",
	        //endDate: "+1y",
	        defaultDate: 'now',
	        //daysOfWeekDisabled: "0",
	        autoclose: true
	    });
	    //$datepicker.datepicker( 'update', new Date() );
		}

	}
	,getVals: function() {
		var vals = {};
		var form = $('form.spl-report-control').serializeArray();
		//return form;
		$.each(form, function() {
			if ( this.name.indexOf('[]' > -1) ) {
				name = this.name.replace('[]', '');
				console.log(this.name);
				//vals[name][] = this.value || '';
			} else {
				//vals[this.name] = this.value || '';
			}

			vals[this.name] = this.value || '';
		});
		return vals;
	}
	,getReport: function() {
		this.api.data.params.vals = this.getVals();
		$('.spl-report-result').html('
Please wait…
');
		$.ajax(
			_this.api
		)
		.done(function(obj) {
			_this.showReport(obj);
		})
		.fail(function() {
		})
		.always(function() {
		});

	}
	,showReport: function(report) {
		//console.log(result);
		this.tmpl = Handlebars.compile( $('.spl-report-tmpl').html() );
		$('.spl-report-result').html( this.tmpl({ report:report }));

		$('.dynatable').dynatable({

			features: {
		    paginate: false,
		    sort: true,
		    pushState: false,
		    search: false,
		    recordCount: false,
		    perPageSelect: false
		  }
		});

	}

}.init();<commit_msg>array<commit_after>var splReport = {

	api: {
		url: '//staff.spokanelibrary.org/wordpress/wp-admin/admin-ajax.php'
		,data: { action: 'spl_reports'
						,nonce: $('.spl-report').data('spl-report-nonce')
						,params: {
							ajax: true
							,id: $('.spl-report').data('spl-report-id')
					}
		}
		,type: 'POST'
		,dataType: 'json'
	}

	,init: function() {
		_this = this;
		this.initUI();
	}
	,initUI: function() {
		$('body').on('submit', '.spl-report-control', function(e) {
			if ( $(this).data('noajax') ) {
			} else {
				e.preventDefault();
				_this.getReport();
			}
		});

		if ( $('#spl-report-json').length > 0 ) {
      var $report = $('#spl-report-json');
      //if ( $account && $account.text().length > 0 ) {
      if ( $report && $report.html().length > 0 ) {
        //var user = JSON.parse($account.text());
        var report = JSON.parse($report.html());
      	console.log(report);
        _this.showReport(report);
      }
    }

		$datepicker = $('.date');
		if ( $datepicker.length ) {
			$datepicker.datepicker({
	        //startDate: "+1d",
	        //endDate: "+1y",
	        defaultDate: 'now',
	        //daysOfWeekDisabled: "0",
	        autoclose: true
	    });
	    //$datepicker.datepicker( 'update', new Date() );
		}

	}
	,getVals: function() {
		var vals = {};
		var form = $('form.spl-report-control').serializeArray();
		//return form;
		$.each(form, function() {
			if ( this.name.indexOf("[]" > -1) ) {
				//name = this.name.replace('[]', '');
				console.log(this.name);
				//vals[name][] = this.value || '';
			} else {
				//vals[this.name] = this.value || '';
			}

			vals[this.name] = this.value || '';
		});
		return vals;
	}
	,getReport: function() {
		this.api.data.params.vals = this.getVals();
		$('.spl-report-result').html('
Please wait…
');
		$.ajax(
			_this.api
		)
		.done(function(obj) {
			_this.showReport(obj);
		})
		.fail(function() {
		})
		.always(function() {
		});

	}
	,showReport: function(report) {
		//console.log(result);
		this.tmpl = Handlebars.compile( $('.spl-report-tmpl').html() );
		$('.spl-report-result').html( this.tmpl({ report:report }));

		$('.dynatable').dynatable({

			features: {
		    paginate: false,
		    sort: true,
		    pushState: false,
		    search: false,
		    recordCount: false,
		    perPageSelect: false
		  }
		});

	}

}.init();<|endoftext|>


<commit_before>const { URL } = require('url') ;

var adapterFor = (function() {
    adapters = {
      'http:': require('http'),
      'https:': require('https'),
    };

  return function(protocol) {
    return adapters[protocol];
  }
}());

var Headers = require('./headers').Headers;

var QUERY_STATE_CHECK_INTERVAL = 800; // 800ms

exports.version = 'unknown';

var Client = exports.Client = function(args){
  if (!args)
    args = {};
  // exports.version set in index.js of project root
  this.userAgent = 'presto-client-node ' + exports.version;

  this.host = args.host || 'localhost';
  this.port = args.port || 8080;
  this.user = args.user || process.env.USER;
  this.basic_auth = args.basic_auth || null;
  this.protocol = 'http:';

  this.catalog = args.catalog;
  this.schema = args.schema;

  this.checkInterval = args.checkInterval || QUERY_STATE_CHECK_INTERVAL;
  this.enableVerboseStateCallback = args.enableVerboseStateCallback || false;
  this.jsonParser = args.jsonParser || JSON;

  if (args.ssl) {
    this.protocol = 'https:';
    this.ssl = args.ssl;
  }
};

Client.prototype.request = function(opts, callback) {
  var client = this;
  var adapter = adapterFor(client.protocol);
  var contentBody = null;

  if (opts instanceof Object) {
    opts.host = client.host;
    opts.port = client.port;
    opts.protocol = client.protocol;
    opts = Object.assign({}, opts, client.ssl);

    if (! opts.headers)
      opts.headers = {};

    if (client.user)
      opts.headers[Headers.USER] = client.user;

    opts.headers[Headers.USER_AGENT] = this.userAgent;

    /**
     * Apply an Authorization header if the user
     * has specified a client.basic_auth object.
     * client.basic_auth = { user: "<user", password : "<password>"}
     */
    if (client.basic_auth)
      opts.headers[Headers.AUTHORIZATION] = 'Basic ' + new Buffer(client.basic_auth.user + ":" + client.basic_auth.password).toString("base64");

    if (opts.body)
      contentBody = opts.body;

    opts.agent = new adapter.Agent(opts); // Otherwise SSL params are ignored.
  } else {
    /**
     * The request has not come through as an object,
     * it is a nextUri string. Hence, if basic auth is to be applied
     * to the request, we must parse the
     * incoming string (opts) and form a new request object with
     * appropriate basic auth headers
     */
    if (client.basic_auth){
      var href = new URL(opts);
      opts = {};
      opts.host = href.hostname;
      opts.port = href.port;
      opts.protocol = client.protocol;
      opts.path = href.pathname;
      opts.headers = {};
      opts.headers[Headers.AUTHORIZATION] = 'Basic ' + new Buffer(client.basic_auth.user + ":" + client.basic_auth.password).toString("base64");
    }
    /**
     * Otherwise, if basic auth not required, continue
     * with request as normal leaving the opts string
     * unmodified for the below adapter.request() call
     */
  }

  var parser = this.jsonParser;

  var req = adapter.request(opts, function(res){
    var response_code = res.statusCode;
    var response_data = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk){
      response_data += chunk;
    });
    res.on('end', function(){
      var data = response_data;
      if (response_code < 300 && (data[0] === '{' || data[0] === '[')) {
        try { data = parser.parse(data); }
        catch (x) {
          /* ignore json parse error (and don't parse) for non-json content body */
        }
      }
      callback(null, response_code, data);
    });
  });

  req.on('error', function(e){
    callback(e);
  });

  if (contentBody)
    req.write(contentBody);

  req.end();
};

Client.prototype.nodes = function(opts, callback) { // TODO: "failed" nodes not supported yet
  if (! callback) {
    callback = opts;
    opts = {};
  }

  this.request({ method: 'GET', path: '/v1/node' }, function(error, code, data){
    if (error || code !== 200) {
      var message = "node list api returns error" + (data && data.length > 0 ? ":" + data : "");
      callback({message: message, error: error, code: code});
      return;
    }
    callback(null, data);
  });
};

Client.prototype.query = function(query_id, callback) {
  this.request({ method: 'GET', path: '/v1/query/' + query_id }, function(error, code, data){
    if (error || code !== 200) {
      var message = "query info api returns error" + (data && data.length > 0 ? ":" + data : "");
      callback({message: message, error: error, code: code});
      return;
    }
    callback(null, data);
  });
};

Client.prototype.kill = function(query_id, callback) {
  this.request({ method: 'DELETE', path: '/v1/query/' + query_id }, function(error, code, data){
    if (error || code !== 204) {
      var message = "query kill api returns error" + (data && data.length > 0 ? ":" + data : "");
      callback({message: message, error: error, code: code});
      return;
    }
    if (callback)
      callback(null);
  });
};

Client.prototype.execute = function(opts) {
  this.statementResource(opts);
};

Client.prototype.statementResource = function(opts) {
  var client = this;
  var columns = null;

  if (!opts.catalog && !this.catalog)
    throw {message: "catalog not specified"};
  if (!opts.schema && !this.schema)
    throw {message: "schema not specified"};
  if (!opts.success && !opts.callback)
    throw {message: "callback function 'success' (or 'callback') not specified"};

  var header = {};
  header[Headers.CATALOG] = opts.catalog || this.catalog;
  header[Headers.SCHEMA] = opts.schema || this.schema;

  if (opts.session)
    header[Headers.SESSION] = opts.session;
  if (opts.timezone)
    header[Headers.TIME_ZONE] = opts.timezone;

  var fetch_info = opts.info || false;

  var cancel_checker = opts.cancel;
  var state_callback = opts.state;
  var columns_callback = opts.columns;
  var data_callback = opts.data;
  var success_callback = opts.success || opts.callback;
  var error_callback = opts.error || opts.callback;

  var enable_verbose_state_callback = this.enableVerboseStateCallback || false;

  var req = { method: 'POST', path: '/v1/statement', headers: header, body: opts.query };
  client.request(req, function(err, code, data){
    if (err || code !== 200 || (data && data.error)) {
      if (error_callback) {
        var message = "execution error" + (data && data.length > 0 ? ":" + data : "");
        if (data && data.error && data.error.message)
          message = data.error.message;
        error_callback({message:message, error: (err || data.error), code: code});
      }
      return;
    }
    /*
    var data = {
      "stats": {
        "processedBytes": 0, "processedRows": 0,
        "wallTimeMillis": 0, "cpuTimeMillis": 0, "userTimeMillis": 0,
        "state": "QUEUED",
        "scheduled": false,
        "nodes": 0,
        "totalSplits": 0, "queuedSplits": 0, "runningSplits": 0, "completedSplits": 0,
      },
      "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/1",
      "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
      "id": "20140120_032523_00000_32v8g"
    };
     */
    if (!data.id || !data.nextUri || !data.infoUri) {
      var error_message = null;
      if (!data.id)
        error_message = "query id missing in response for POST /v1/statement";
      else if (!data.nextUri)
        error_message = "nextUri missing in response for POST /v1/statement";
      else if (!data.infoUri)
        error_message = "infoUri missing in response for POST /v1/statement";
      error_callback({message: error_message, data: data});
      return;
    }
    var last_state = null;
    var firstNextUri = data.nextUri; // TODO: check the cases without nextUri for /statement ?
    var fetch_next = function(next_uri){
      /*
       * 1st time
{
 "stats": {
   "rootStage": {
     "subStages": [
       {
         "subStages": [],
         "processedBytes": 83103149, "processedRows": 2532704,
         "wallTimeMillis": 20502, "cpuTimeMillis": 3431, "userTimeMillis": 3210,
         "stageId": "1", "state": "FINISHED", "done": true,
         "nodes": 3,
         "totalSplits": 420, "queuedSplits": 0, "runningSplits": 0, "completedSplits": 420
       }
     ],
     // same as substage
   },
   // same as substage
   "state": "RUNNING",
 },
 "data": [ [ 1266352 ] ],
 "columns": [ { "type": "bigint", "name": "cnt" } ],
 "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/2",
 "partialCancelUri": "http://10.0.0.0:8080/v1/stage/20140120_032523_00000_32v8g.0",
 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
 "id": "20140120_032523_00000_32v8g"
}
       */
      /*
       * 2nd time
{
 "stats": {
   // ....
   "state": "FINISHED",
 },
 "columns": [ { "type": "bigint", "name": "cnt" } ],
 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
 "id": "20140120_032523_00000_32v8g"
}
       */
      if (cancel_checker && cancel_checker()) {
        client.request({ method: 'DELETE', path: next_uri }, function(error, code, data){
          if (error || code !== 204) {
            error_callback({message: "query fetch canceled, but Presto query cancel may fail", error: error, code: code});
          } else {
            error_callback({message: "query fetch canceled by operation"});
          }
        });
        return;
      }
      client.request(next_uri, function(error, code, response){
        if (error || response.error) {
          error_callback(error || response.error);
          return;
        }

        if (state_callback && (last_state !== response.stats.state || enable_verbose_state_callback)) {
          state_callback(null, response.id, response.stats);
          last_state = response.stats.state;
        }

        if (columns_callback && response.columns && !columns) {
          columns = response.columns;
          columns_callback(null, columns);
        }

        /* presto-main/src/main/java/com/facebook/presto/execution/QueryState.java
         * QUEUED, PLANNING, STARTING, RUNNING, FINISHED, CANCELED, FAILED
         */
        if (response.stats.state === 'QUEUED'
          || response.stats.state === 'PLANNING'
          || response.stats.state === 'STARTING'
          || response.stats.state === 'RUNNING' && !response.data) {
          var next = response.nextUri;
          setTimeout(function(){ fetch_next(next); }, client.checkInterval);
          return;
        }

        if (data_callback && response.data) {
          data_callback(null, response.data, response.columns, response.stats);
        }

        if (response.nextUri) {
          var next = response.nextUri;
          setTimeout(function(){ fetch_next(next); }, client.checkInterval);
          return;
        }

        var finishedStats = response.stats;

        if (fetch_info && response.infoUri) {
          client.request(response.infoUri, function(error, code, response){
            success_callback(null, finishedStats, response);
          });
        }
        else {
          success_callback(null, finishedStats);
        }
      });
    };
    fetch_next(firstNextUri);
  });
};
<commit_msg>Adding support for setting source in query request<commit_after>const { URL } = require('url') ;

var adapterFor = (function() {
    adapters = {
      'http:': require('http'),
      'https:': require('https'),
    };

  return function(protocol) {
    return adapters[protocol];
  }
}());

var Headers = require('./headers').Headers;

var QUERY_STATE_CHECK_INTERVAL = 800; // 800ms

exports.version = 'unknown';

var Client = exports.Client = function(args){
  if (!args)
    args = {};
  // exports.version set in index.js of project root
  this.userAgent = 'presto-client-node ' + exports.version;

  this.host = args.host || 'localhost';
  this.port = args.port || 8080;
  this.user = args.user || process.env.USER;
  this.basic_auth = args.basic_auth || null;
  this.protocol = 'http:';

  this.catalog = args.catalog;
  this.schema = args.schema;

  this.source = args.source || 'nodejs-client';

  this.checkInterval = args.checkInterval || QUERY_STATE_CHECK_INTERVAL;
  this.enableVerboseStateCallback = args.enableVerboseStateCallback || false;
  this.jsonParser = args.jsonParser || JSON;

  if (args.ssl) {
    this.protocol = 'https:';
    this.ssl = args.ssl;
  }
};

Client.prototype.request = function(opts, callback) {
  var client = this;
  var adapter = adapterFor(client.protocol);
  var contentBody = null;

  if (opts instanceof Object) {
    opts.host = client.host;
    opts.port = client.port;
    opts.protocol = client.protocol;
    opts = Object.assign({}, opts, client.ssl);

    if (! opts.headers)
      opts.headers = {};

    if (client.user)
      opts.headers[Headers.USER] = client.user;

    if (client.source)
      opts.headers[Headers.SOURCE] = client.source;

    opts.headers[Headers.USER_AGENT] = this.userAgent;

    /**
     * Apply an Authorization header if the user
     * has specified a client.basic_auth object.
     * client.basic_auth = { user: "<user", password : "<password>"}
     */
    if (client.basic_auth)
      opts.headers[Headers.AUTHORIZATION] = 'Basic ' + new Buffer(client.basic_auth.user + ":" + client.basic_auth.password).toString("base64");

    if (opts.body)
      contentBody = opts.body;

    opts.agent = new adapter.Agent(opts); // Otherwise SSL params are ignored.
  } else {
    /**
     * The request has not come through as an object,
     * it is a nextUri string. Hence, if basic auth is to be applied
     * to the request, we must parse the
     * incoming string (opts) and form a new request object with
     * appropriate basic auth headers
     */
    if (client.basic_auth){
      var href = new URL(opts);
      opts = {};
      opts.host = href.hostname;
      opts.port = href.port;
      opts.protocol = client.protocol;
      opts.path = href.pathname;
      opts.headers = {};
      opts.headers[Headers.AUTHORIZATION] = 'Basic ' + new Buffer(client.basic_auth.user + ":" + client.basic_auth.password).toString("base64");
    }
    /**
     * Otherwise, if basic auth not required, continue
     * with request as normal leaving the opts string
     * unmodified for the below adapter.request() call
     */
  }

  var parser = this.jsonParser;

  var req = adapter.request(opts, function(res){
    var response_code = res.statusCode;
    var response_data = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk){
      response_data += chunk;
    });
    res.on('end', function(){
      var data = response_data;
      if (response_code < 300 && (data[0] === '{' || data[0] === '[')) {
        try { data = parser.parse(data); }
        catch (x) {
          /* ignore json parse error (and don't parse) for non-json content body */
        }
      }
      callback(null, response_code, data);
    });
  });

  req.on('error', function(e){
    callback(e);
  });

  if (contentBody)
    req.write(contentBody);

  req.end();
};

Client.prototype.nodes = function(opts, callback) { // TODO: "failed" nodes not supported yet
  if (! callback) {
    callback = opts;
    opts = {};
  }

  this.request({ method: 'GET', path: '/v1/node' }, function(error, code, data){
    if (error || code !== 200) {
      var message = "node list api returns error" + (data && data.length > 0 ? ":" + data : "");
      callback({message: message, error: error, code: code});
      return;
    }
    callback(null, data);
  });
};

Client.prototype.query = function(query_id, callback) {
  this.request({ method: 'GET', path: '/v1/query/' + query_id }, function(error, code, data){
    if (error || code !== 200) {
      var message = "query info api returns error" + (data && data.length > 0 ? ":" + data : "");
      callback({message: message, error: error, code: code});
      return;
    }
    callback(null, data);
  });
};

Client.prototype.kill = function(query_id, callback) {
  this.request({ method: 'DELETE', path: '/v1/query/' + query_id }, function(error, code, data){
    if (error || code !== 204) {
      var message = "query kill api returns error" + (data && data.length > 0 ? ":" + data : "");
      callback({message: message, error: error, code: code});
      return;
    }
    if (callback)
      callback(null);
  });
};

Client.prototype.execute = function(opts) {
  this.statementResource(opts);
};

Client.prototype.statementResource = function(opts) {
  var client = this;
  var columns = null;

  if (!opts.catalog && !this.catalog)
    throw {message: "catalog not specified"};
  if (!opts.schema && !this.schema)
    throw {message: "schema not specified"};
  if (!opts.success && !opts.callback)
    throw {message: "callback function 'success' (or 'callback') not specified"};

  var header = {};
  header[Headers.CATALOG] = opts.catalog || this.catalog;
  header[Headers.SCHEMA] = opts.schema || this.schema;

  if (opts.session)
    header[Headers.SESSION] = opts.session;
  if (opts.timezone)
    header[Headers.TIME_ZONE] = opts.timezone;

  var fetch_info = opts.info || false;

  var cancel_checker = opts.cancel;
  var state_callback = opts.state;
  var columns_callback = opts.columns;
  var data_callback = opts.data;
  var success_callback = opts.success || opts.callback;
  var error_callback = opts.error || opts.callback;

  var enable_verbose_state_callback = this.enableVerboseStateCallback || false;

  var req = { method: 'POST', path: '/v1/statement', headers: header, body: opts.query };
  client.request(req, function(err, code, data){
    if (err || code !== 200 || (data && data.error)) {
      if (error_callback) {
        var message = "execution error" + (data && data.length > 0 ? ":" + data : "");
        if (data && data.error && data.error.message)
          message = data.error.message;
        error_callback({message:message, error: (err || data.error), code: code});
      }
      return;
    }
    /*
    var data = {
      "stats": {
        "processedBytes": 0, "processedRows": 0,
        "wallTimeMillis": 0, "cpuTimeMillis": 0, "userTimeMillis": 0,
        "state": "QUEUED",
        "scheduled": false,
        "nodes": 0,
        "totalSplits": 0, "queuedSplits": 0, "runningSplits": 0, "completedSplits": 0,
      },
      "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/1",
      "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
      "id": "20140120_032523_00000_32v8g"
    };
     */
    if (!data.id || !data.nextUri || !data.infoUri) {
      var error_message = null;
      if (!data.id)
        error_message = "query id missing in response for POST /v1/statement";
      else if (!data.nextUri)
        error_message = "nextUri missing in response for POST /v1/statement";
      else if (!data.infoUri)
        error_message = "infoUri missing in response for POST /v1/statement";
      error_callback({message: error_message, data: data});
      return;
    }
    var last_state = null;
    var firstNextUri = data.nextUri; // TODO: check the cases without nextUri for /statement ?
    var fetch_next = function(next_uri){
      /*
       * 1st time
{
 "stats": {
   "rootStage": {
     "subStages": [
       {
         "subStages": [],
         "processedBytes": 83103149, "processedRows": 2532704,
         "wallTimeMillis": 20502, "cpuTimeMillis": 3431, "userTimeMillis": 3210,
         "stageId": "1", "state": "FINISHED", "done": true,
         "nodes": 3,
         "totalSplits": 420, "queuedSplits": 0, "runningSplits": 0, "completedSplits": 420
       }
     ],
     // same as substage
   },
   // same as substage
   "state": "RUNNING",
 },
 "data": [ [ 1266352 ] ],
 "columns": [ { "type": "bigint", "name": "cnt" } ],
 "nextUri": "http://localhost:8080/v1/statement/20140120_032523_00000_32v8g/2",
 "partialCancelUri": "http://10.0.0.0:8080/v1/stage/20140120_032523_00000_32v8g.0",
 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
 "id": "20140120_032523_00000_32v8g"
}
       */
      /*
       * 2nd time
{
 "stats": {
   // ....
   "state": "FINISHED",
 },
 "columns": [ { "type": "bigint", "name": "cnt" } ],
 "infoUri": "http://localhost:8080/v1/query/20140120_032523_00000_32v8g",
 "id": "20140120_032523_00000_32v8g"
}
       */
      if (cancel_checker && cancel_checker()) {
        client.request({ method: 'DELETE', path: next_uri }, function(error, code, data){
          if (error || code !== 204) {
            error_callback({message: "query fetch canceled, but Presto query cancel may fail", error: error, code: code});
          } else {
            error_callback({message: "query fetch canceled by operation"});
          }
        });
        return;
      }
      client.request(next_uri, function(error, code, response){
        if (error || response.error) {
          error_callback(error || response.error);
          return;
        }

        if (state_callback && (last_state !== response.stats.state || enable_verbose_state_callback)) {
          state_callback(null, response.id, response.stats);
          last_state = response.stats.state;
        }

        if (columns_callback && response.columns && !columns) {
          columns = response.columns;
          columns_callback(null, columns);
        }

        /* presto-main/src/main/java/com/facebook/presto/execution/QueryState.java
         * QUEUED, PLANNING, STARTING, RUNNING, FINISHED, CANCELED, FAILED
         */
        if (response.stats.state === 'QUEUED'
          || response.stats.state === 'PLANNING'
          || response.stats.state === 'STARTING'
          || response.stats.state === 'RUNNING' && !response.data) {
          var next = response.nextUri;
          setTimeout(function(){ fetch_next(next); }, client.checkInterval);
          return;
        }

        if (data_callback && response.data) {
          data_callback(null, response.data, response.columns, response.stats);
        }

        if (response.nextUri) {
          var next = response.nextUri;
          setTimeout(function(){ fetch_next(next); }, client.checkInterval);
          return;
        }

        var finishedStats = response.stats;

        if (fetch_info && response.infoUri) {
          client.request(response.infoUri, function(error, code, response){
            success_callback(null, finishedStats, response);
          });
        }
        else {
          success_callback(null, finishedStats);
        }
      });
    };
    fetch_next(firstNextUri);
  });
};
<|endoftext|>


<commit_before>// @flow
import React from 'react'
import {
  Alert,
  ListView,
  Modal,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native'
import expect from 'expect'
import { connect } from 'react-redux'
import PropTypes from 'prop-types';

import SRRatingView from './SRRatingView'
import SRStudyListCell from './SRStudyListCell'
import SRTypographicCell from './SRTypographicCell'
import { SRSGrade } from './SRSpacedRepetition'
import { processDataForList } from './dataModel/SRDataPresenter'
import { SRDarkColor, SRYellowColor, SRBrightColor, SRRedColor } from './utilities/SRColors'
import { formatCellDate } from './utilities/SRDateFormat'
import SREmptyStateHeader from './components/SREmptyStateHeader'
import SREmptyState from './components/SREmptyState'

export const studyListTitle = 'Reviews'

class SRStudyList extends React.Component {

  state: {
    dataSource: ListView.DataSource,
    ratingModalisVisible: bool,
    renderEmptyStateHeader: bool,
    emptyStateTableDistanceFromBottom: number,
    selectedID: string,
    listViewHeight: number,
    onlyTypographicCellHeight: number,
  }

  constructor(props: Object) {
    super(props);
    const dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});

    this.state = {
      dataSource: dataSource.cloneWithRows([]),
      ratingModalisVisible: false,
      selectedID: '',
      renderEmptyStateHeader: false,
      emptyStateTableDistanceFromBottom: 0,
      listViewHeight: 0,
      onlyTypographicCellHeight: 0,
    }
  }

  componentWillMount() {
    this.updateUIStates(this.props)
  }

  componentWillReceiveProps(newProps: Object) {
    this.updateUIStates(newProps)
  }

  render() {
    const {
      dataSource,
      renderEmptyStateHeader,
      ratingModalisVisible
    } = this.state

    return (
      <View style={styles.container}>
        {this._renderEmptyStateHeader(renderEmptyStateHeader)}
        <ListView
          contentContainerStyle={styles.tableViewContainer}
          style={styles.tableView}
          dataSource={dataSource}
          enableEmptySections={true}
          onLayout={(event) => {
            const {x, y, width, height} = event.nativeEvent.layout
            this.setState({listViewHeight: height}) // only called once when it changes
          }}
          renderRow={(item, sectionID, rowID, highlightRow) => {

            const d = new Date(item.date)
            const itemIsOverDue = new Date() > d
            const itemIsToday = d.toDateString() == new Date().toDateString()
            const allowRating = itemIsToday || itemIsOverDue
            const formattedDate = formatCellDate(d)

            if(allowRating) {
              return (
                // needs wrapper because onLayout doesn't get called on SRTypographicCell for some reason
                <View style={{flex:1}}
                  onLayout={(event) => {
                    const {x, y, width, height} = event.nativeEvent.layout
                    if(rowID == 0) {
                      this.setState({onlyTypographicCellHeight: height}) // only called once when it changes
                    }
                  }}>
                  <SRTypographicCell
                    onPressDetailsButton={() => this.navigateToDetails(item)}
                    onPressRateButton={() => this.rateItem(item.id)}
                  >
                    {{title: item.taskName, notes: item.notes, date: formattedDate}}
                  </SRTypographicCell>
                </View>
              )
            } else {
              return (

                <SRStudyListCell
                  onPressDetailsButton={() => this.navigateToDetails(item)}
                >
                {{title: item.taskName, notes: item.notes, date: formattedDate}}
              </SRStudyListCell>

            )}
          }}
        />

        {this._renderEmptyStateTable()}

        <Modal
          animationType={"fade"}
          transparent={true}
          visible={ratingModalisVisible}
          >
         <SRRatingView
           dismissAction={() => this.setRatingModalVisible(!ratingModalisVisible)}
           ratedCallback={(index) => this.rateTask(index)}
         />
        </Modal>

      </View>
    )
  }

  // UI state

  updateUIStates = (props) => {
    console.log(`XXX updateUIStates`)
    const { tableData, studyTasks } = props

    const noDataYet = tableData.length == 0
    var showEmptyStateHeader = false

    if(noDataYet) {
      showEmptyStateHeader = true
    } else {
      const firstItem = tableData[0]
      const onlyFutureTasks = (new Date(firstItem.date) > new Date())
      if(onlyFutureTasks) {
        showEmptyStateHeader = true
      }
    }

    this.setState({
      dataSource: this.state.dataSource.cloneWithRows(tableData),
      renderEmptyStateHeader: showEmptyStateHeader,
    })
  }

  _renderEmptyStateHeader = (flag) => {
    if(flag) {
      return (
        <SREmptyStateHeader />
      )
    } else {
      return null
    }
  }

  _renderEmptyStateTable = () => {
    console.log(`XXX _renderEmptyStateTable`)
    const { listViewHeight, onlyTypographicCellHeight } = this.state
    const { tableData } = this.props

    var shouldRender = false
    var bottom = 0

    if(listViewHeight != 0) {
      const { studyTasks } = this.props
      const noDataYet = tableData.length == 0

      if(noDataYet) {
        shouldRender = true
        bottom = listViewHeight / 2
      } else {
        const firstItem = tableData[0]
        const onlyFutureTasks = (new Date(firstItem.date) > new Date())

        if(onlyFutureTasks) {

        } else {
          if(tableData.length == 1 && onlyTypographicCellHeight != 0) {
            shouldRender = true
            bottom = (listViewHeight - onlyTypographicCellHeight) / 2
          }
        }
      }
    }

    if(shouldRender) {
      return (
        <View style={{
          position: 'absolute',
          alignItems: 'center',
          right: 0,
          left: 0,
          bottom:  bottom,
        }}>
          <SREmptyState style={{
            // flex:1,
            // justifyContent: 'center',
            // alignItems: 'center',
          }}/>
        </View>
      )
    } else {
      return null
    }
  }

  rateItem = (id: string) => {
    this.setState({selectedID: id})
    this.setRatingModalVisible(true)
  }

  setRatingModalVisible(visible: bool) {
    this.setState({ratingModalisVisible: visible});
  }

  rateTask = (index: number) => {
    var grade = -1
    switch(index) {
      case 0:
        grade = SRSGrade.BAD
        break;
      case 1:
        grade = SRSGrade.OK
        break
      case 2:
        grade = SRSGrade.GOOD
        break
      default:
      throw "No valid rating selected"
    }
    const { rateAction } = this.props
    const { selectedID } = this.state
    rateAction(selectedID, grade)
    this.setRatingModalVisible(false)
  }

  navigateToDetails = (item: any) => {
    const { navigationAction } = this.props
    navigationAction(item)
  }

}

SRStudyList.propTypes = {
  rateAction: PropTypes.func,
  navigationAction: PropTypes.func,
}

const mapStateToProps = (state) => {
    return {
        studyTasks: state.studyTasks,
        tableData: processDataForList(state.studyTasks)
    }
}

export default connect(mapStateToProps)(SRStudyList)

const styles = StyleSheet.create({
  container: {
   flex: 1,
  },
  tableViewContainer: {
    paddingBottom: 90,
  },
  tableView: {
    backgroundColor: SRBrightColor,
  },
})
<commit_msg>Added last cell with empty state image to fill table view container bottom padding.<commit_after>// @flow
import React from 'react'
import {
  Alert,
  ListView,
  Modal,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native'
import expect from 'expect'
import { connect } from 'react-redux'
import PropTypes from 'prop-types';

import SRRatingView from './SRRatingView'
import SRStudyListCell from './SRStudyListCell'
import SRTypographicCell from './SRTypographicCell'
import { SRSGrade } from './SRSpacedRepetition'
import { processDataForList } from './dataModel/SRDataPresenter'
import { SRDarkColor, SRYellowColor, SRBrightColor, SRRedColor } from './utilities/SRColors'
import { formatCellDate } from './utilities/SRDateFormat'
import SREmptyStateHeader from './components/SREmptyStateHeader'
import SREmptyState from './components/SREmptyState'

export const studyListTitle = 'Reviews'

class SRStudyList extends React.Component {

  state: {
    dataSource: ListView.DataSource,
    ratingModalisVisible: bool,
    renderEmptyStateHeader: bool,
    emptyStateTableDistanceFromBottom: number,
    selectedID: string,
    listViewHeight: number,
    onlyTypographicCellHeight: number,
  }

  constructor(props: Object) {
    super(props);
    const dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});

    this.state = {
      dataSource: dataSource.cloneWithRows([]),
      ratingModalisVisible: false,
      selectedID: '',
      renderEmptyStateHeader: false,
      emptyStateTableDistanceFromBottom: 0,
      listViewHeight: 0,
      onlyTypographicCellHeight: 0,
    }
  }

  componentWillMount() {
    this.updateUIStates(this.props)
  }

  componentWillReceiveProps(newProps: Object) {
    this.updateUIStates(newProps)
  }

  render() {
    const {
      dataSource,
      renderEmptyStateHeader,
      ratingModalisVisible
    } = this.state

    return (
      <View style={styles.container}>
        {this._renderEmptyStateHeader(renderEmptyStateHeader)}
        <ListView
          contentContainerStyle={styles.tableViewContainer}
          style={styles.tableView}
          dataSource={dataSource}
          enableEmptySections={true}
          onLayout={(event) => {
            const {x, y, width, height} = event.nativeEvent.layout
            this.setState({listViewHeight: height}) // only called once when it changes
          }}
          renderRow={(item, sectionID, rowID, highlightRow) => {

            const d = new Date(item.date)
            const itemIsOverDue = new Date() > d
            const itemIsToday = d.toDateString() == new Date().toDateString()
            const allowRating = itemIsToday || itemIsOverDue
            const formattedDate = formatCellDate(d)
            const isLastCell = rowID == dataSource.getRowCount()-1

            if(allowRating) {
              return (
                // needs wrapper because onLayout doesn't get called on SRTypographicCell for some reason
                <View style={{flex:1}}
                  onLayout={(event) => {
                    const {x, y, width, height} = event.nativeEvent.layout
                    if(rowID == 0) {
                      this.setState({onlyTypographicCellHeight: height}) // only called once when it changes
                    }
                  }}>
                  <SRTypographicCell
                    onPressDetailsButton={() => this.navigateToDetails(item)}
                    onPressRateButton={() => this.rateItem(item.id)}
                  >
                    {{title: item.taskName, notes: item.notes, date: formattedDate}}
                  </SRTypographicCell>
                </View>
              )
            } else {

              const cell = <SRStudyListCell
                  onPressDetailsButton={() => this.navigateToDetails(item)}
                >
                {{title: item.taskName, notes: item.notes, date: formattedDate}}
              </SRStudyListCell>

              if(isLastCell) {
                return(
                  <View>
                    {cell}
                    <View style={styles.emptyStateCell}>
                      <SREmptyState />
                    </View>
                  </View>
                )
              } else {
                return cell
              }
            }
          }}
        />

        {this._renderEmptyStateTable()}

        <Modal
          animationType={"fade"}
          transparent={true}
          visible={ratingModalisVisible}
          >
         <SRRatingView
           dismissAction={() => this.setRatingModalVisible(!ratingModalisVisible)}
           ratedCallback={(index) => this.rateTask(index)}
         />
        </Modal>

      </View>
    )
  }

  // UI state

  updateUIStates = (props) => {
    const { tableData, studyTasks } = props

    const noDataYet = tableData.length == 0
    var showEmptyStateHeader = false

    if(noDataYet) {
      showEmptyStateHeader = true
    } else {
      const firstItem = tableData[0]
      const onlyFutureTasks = (new Date(firstItem.date) > new Date())
      if(onlyFutureTasks) {
        showEmptyStateHeader = true
      }
    }

    this.setState({
      dataSource: this.state.dataSource.cloneWithRows(tableData),
      renderEmptyStateHeader: showEmptyStateHeader,
    })
  }

  _renderEmptyStateHeader = (flag) => {
    if(flag) {
      return (
        <SREmptyStateHeader />
      )
    } else {
      return null
    }
  }

  _renderEmptyStateTable = () => {
    const { listViewHeight, onlyTypographicCellHeight } = this.state
    const { tableData } = this.props

    var shouldRender = false
    var bottom = 0

    if(listViewHeight != 0) {
      const { studyTasks } = this.props
      const noDataYet = tableData.length == 0

      if(noDataYet) {
        shouldRender = true
        bottom = listViewHeight / 2
      } else {
        const firstItem = tableData[0]
        const onlyFutureTasks = (new Date(firstItem.date) > new Date())

        if(onlyFutureTasks) {

        } else {
          if(tableData.length == 1 && onlyTypographicCellHeight != 0) {
            shouldRender = true
            bottom = (listViewHeight - onlyTypographicCellHeight) / 2
          }
        }
      }
    }

    if(shouldRender) {
      return (
        <View style={{
          position: 'absolute',
          alignItems: 'center',
          right: 0,
          left: 0,
          bottom:  bottom,
        }}>
          <SREmptyState />
        </View>
      )
    } else {
      return null
    }
  }

  rateItem = (id: string) => {
    this.setState({selectedID: id})
    this.setRatingModalVisible(true)
  }

  setRatingModalVisible(visible: bool) {
    this.setState({ratingModalisVisible: visible});
  }

  rateTask = (index: number) => {
    var grade = -1
    switch(index) {
      case 0:
        grade = SRSGrade.BAD
        break;
      case 1:
        grade = SRSGrade.OK
        break
      case 2:
        grade = SRSGrade.GOOD
        break
      default:
      throw "No valid rating selected"
    }
    const { rateAction } = this.props
    const { selectedID } = this.state
    rateAction(selectedID, grade)
    this.setRatingModalVisible(false)
  }

  navigateToDetails = (item: any) => {
    const { navigationAction } = this.props
    navigationAction(item)
  }

}

SRStudyList.propTypes = {
  rateAction: PropTypes.func,
  navigationAction: PropTypes.func,
}

const mapStateToProps = (state) => {
    return {
        studyTasks: state.studyTasks,
        tableData: processDataForList(state.studyTasks)
    }
}

export default connect(mapStateToProps)(SRStudyList)

const styles = StyleSheet.create({
  container: {
   flex: 1,
  },
  tableViewContainer: {
    paddingBottom: 0,
  },
  tableView: {
    backgroundColor: SRBrightColor,
  },
  emptyStateCell: {
    alignItems: 'center',
    paddingTop: (90-38)/2, // (floatingButton bottomSpacing + floatingButtonHeight + bottomSpacing) - empty state height
    paddingBottom: (90-38)/2,
  },
})
<|endoftext|>