Source: org/terraswarm/accessor/accessors/web/net/KeyValueStore.js

// Copyright (c) 2016 The Regents of the University of California.
// All rights reserved.

// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.

// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.

// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.

/** This accessor reads or writes data to a key-value store web service whenever
 *  it receives a trigger input.
 *  A URL for the service is specified by the <i>storeLocation</i> parameter.
 *  A Ptolemy II model that provides such a key-value store service can be found
 *  at https://www.icyphy.org/accessors/demo/KeyValueStore/KeyValueStoreServer.xml.
 *  A demo client that uses this accessor is provided at
 *  https://www.icyphy.org/accessors/demo/KeyValueStore/KeyValueStoreClient.xml.
 *
 *  The key and the value are both text items provided as inputs.
 *  If <i>remove</i> is true and the <i>key</i> is non-empty, then upon firing, this actor
 *  will remove the specified key from the store, producing on its output the previous
 *  value (if any). If <i>remove</i> is false, then this actor will either set or
 *  retrieve a value in the key-value store, depending on whether the
 *  <i>value</i> input is non-empty. If the <i>value</i> is non-empty, then this
 *  actor sets the value for the specified key.
 *  If it is empty, then this actor retrieves the value for the specified key.
 *  If no key is given, then this actor retrieves an array of all the keys
 *  in the key-value store.
 *
 *  If an error occurs accessing the key-value store (e.g., no store is found at the specified
 *  URL, or no value is found with the specified key),
 *  then an exception is thrown.
 *
 *  This accessor assumes that the protocol implemented at that location matches
 *  the specification below for the default location:
 *
 *  * To store a value with key MY_ID and value MY_VALUE, use
 *
 *      http://localhost:8077/keyvalue/set?id=MY_ID&value=MY_VALUE
 *
 *  * To retrieve the value, use
 *
 *      http://localhost:8077/keyvalue/get?id=MY_ID
 *
 *  * To remove a value, use
 *
 *      http://localhost:8077/keyvalue/delete?id=MY_ID
 *
 *  * To list all the keys, use
 *
 *      http://localhost:8077/keyvalue/list
 *
 *  The key and value are both
 *  encoded using the JavaScript encodeURIComponent() function,
 *  and on retrieval, decoded using decodeURIComponent(),
 *  and hence can include any text characters.
 *
 *  Note that this accessor uses nonblocking reads to access the store,
 *  so the output is produced later when the server responds.
 *
 *  @accessor net/KeyValueStore
 *  @input {string} storeLocation The URL of the key-value store service.
 *  @input {string} key The key to be updated or retrieved.
 *  @input {boolean} remove If true, then remove the key from the store;
 *   otherwise, retrieve the value for the key.
 *  @input {string} value The value to store in the key-value store,
 *   or empty to not store anything.
 *  @input trigger The trigger input.
 *  @output {string} result The value retrieved from or written to
 *   the key-value store.
 *
 *  @author Edward A. Lee
 *  @version $$Id$$
 */

// Stop extra messages from jslint.  Note that there should be no
// space between the / and the * and global.
/*global console, error, exports, readURL */
/*jshint globalstrict: true */
"use strict";

var httpClient = require('@accessors-modules/http-client');

exports.setup = function () {
    this.input('storeLocation', {
        'type': 'string',
        'value': 'http://localhost:8077/keyvalue'
    });
    this.input('key', {
        'type': 'string',
        'value': ''
    });
    this.input('remove', {
        'type': 'boolean',
        'value': false
    });
    this.input('value', {
        'type': 'string'
    });
    this.input('trigger');
    this.output('result', {
        'type': 'string',
        'spontaneous': true
    });
};

var inputHandler = null;
exports.initialize = function() {
    inputHandler = this.addInputHandler('trigger', handleInputs.bind(this));
}

function handleInputs() {
    var store = this.get('storeLocation');
    var theKey = this.get('key');
    var toRemove = this.get('remove');
    var theValue = this.get('value');
    var thiz = this;
    if (toRemove) {
        if (theKey !== "") {
            httpClient.get(url, function(response) {
                var produce = response.body;
                var url = store + '/delete?id=' + theKey;
                // FIXME: This should use HTTP delete not get.
                httpClient.get(url, function(response) {
                    if (checkResponse(response, thiz) && produce !== "") {
                        thiz.send('result', produce);
                    }
                });
            });
        }
    } else {
        // toRemove == false. If there is a value, use it to set.
        if (theValue !== "" && theValue !== null) {
            if (!theKey) {
                thiz.error("Invalid key: " + theKey + " for value: " + theValue);
                return;
            }
            // FIXME: encodeURIComponent is not defined as a top-level accessor function.
            var url = store + '/set?id=' + encodeURIComponent(theKey);
            var options = {
                'url':url,
                'body':theValue
            };
            httpClient.post(options, function(response) {
                if (checkResponse(response, thiz)) {
                    thiz.send('result', theValue);
                }
            });
        } else {
            var url;
            if (theKey) {
                url = store + '/get?id=' + theKey;
            } else {
                url = store + '/list';
            }
            httpClient.get(url, function(response) {
                if (checkResponse(response, thiz)) {
                    var valueFromStore = decodeURIComponent(response.body);
                    thiz.send('result', valueFromStore);
                }
            });
        }
    }
};

function checkResponse(response, thiz) {
    if (response.statusCode >= 400) {
        thiz.error('Server responds with '
                + response.statusCode
                + ': '
                + response.statusMessage);
        return false;
    } else if (response.statusCode >= 300) {
        thiz.error('Server responds with a redirect, no supported yet, code '
                + response.statusCode
                + ': '
                + response.statusMessage);
        return false;
    }
    return true;
}

exports.wrapup = function() {
    if (inputHandler !== null) {
        this.removeInputHandler(inputHandler);
        inputHandler = null;
    }
}