Merge branch 'master' of https://github.com/highfidelity/hifi into priority

This commit is contained in:
Andrzej Kapolka 2014-05-14 11:01:10 -07:00
commit c99f3edbd8
6 changed files with 632 additions and 0 deletions

68
examples/Test.js Normal file
View file

@ -0,0 +1,68 @@
//
// Test.js
// examples
//
// Created by Ryan Huffman on 5/4/14
// Copyright 2014 High Fidelity, Inc.
//
// This provides very basic unit testing functionality.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
test = function(name, func) {
print("Running test: " + name);
var unitTest = new UnitTest(name, func);
try {
unitTest.run();
print(" Success: " + unitTest.numAssertions + " assertions passed");
} catch (error) {
print(" Failure: " + error.message);
}
};
AssertionException = function(expected, actual, message) {
print("Creating exception");
this.message = message + "\n: " + actual + " != " + expected;
this.name = 'AssertionException';
};
UnitTest = function(name, func) {
this.numAssertions = 0;
this.func = func;
};
UnitTest.prototype.run = function() {
this.func();
};
UnitTest.prototype.assertNotEquals = function(expected, actual, message) {
this.numAssertions++;
if (expected == actual) {
throw new AssertionException(expected, actual, message);
}
};
UnitTest.prototype.assertEquals = function(expected, actual, message) {
this.numAssertions++;
if (expected != actual) {
throw new AssertionException(expected, actual, message);
}
};
UnitTest.prototype.assertHasProperty = function(property, actual, message) {
this.numAssertions++;
if (actual[property] === undefined) {
throw new AssertionException(property, actual, message);
}
};
UnitTest.prototype.assertNull = function(value, message) {
this.numAssertions++;
if (value !== null) {
throw new AssertionException(value, null, message);
}
};

View file

@ -0,0 +1,51 @@
//
// streetAreaExample.js
// examples
//
// Created by Ryan Huffman on 5/4/14
// Copyright 2014 High Fidelity, Inc.
//
// This is an example script showing how to load JSON data using XMLHttpRequest.
//
// URL Macro created by Thijs Wenker.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var url = "https://script.google.com/macros/s/AKfycbwIo4lmF-qUwX1Z-9eA_P-g2gse9oFhNcjVyyksGukyDDEFXgU/exec?action=listOwners&domain=alpha.highfidelity.io";
print("Loading street data from " + url);
var req = new XMLHttpRequest();
// Set response type to "json". This will tell XMLHttpRequest to parse the response data as json, so req.response can be used
// as a regular javascript object
req.responseType = 'json';
req.open("GET", url, false);
req.send();
if (req.status == 200) {
for (var domain in req.response) {
print("DOMAIN: " + domain);
var locations = req.response[domain];
var userAreas = [];
for (var i = 0; i < locations.length; i++) {
var loc = locations[i];
var x1 = loc[1],
x2 = loc[2],
y1 = loc[3],
y2 = loc[4];
userAreas.push({
username: loc[0],
area: Math.abs(x2 - x1) * Math.abs(y2 - y1),
});
}
userAreas.sort(function(a, b) { return a.area > b.area ? -1 : (a.area < b.area ? 1 : 0) });
for (var i = 0; i < userAreas.length; i++) {
print(userAreas[i].username + ": " + userAreas[i].area + " sq units");
}
}
} else {
print("Error loading data: " + req.status + " " + req.statusText + ", " + req.errorCode);
}

View file

@ -0,0 +1,147 @@
//
// testXMLHttpRequest.js
// examples
//
// Created by Ryan Huffman on 5/4/14
// Copyright 2014 High Fidelity, Inc.
//
// XMLHttpRequest Tests
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
Script.include("Test.js");
test("Test default request values", function(finished) {
var req = new XMLHttpRequest();
this.assertEquals(req.UNSENT, req.readyState, "readyState should be UNSENT");
this.assertEquals(0, req.status, "status should be `0` by default");
this.assertEquals("", req.statusText, "statusText should be empty string by default");
this.assertEquals("", req.getAllResponseHeaders(), "getAllResponseHeaders() should return empty string by default");
this.assertEquals("", req.response, "response should be empty string by default");
this.assertEquals("", req.responseText, "responseText should be empty string by default");
this.assertEquals("", req.responseType, "responseType should be empty string by default");
this.assertEquals(0, req.timeout, "timeout should be `0` by default");
this.assertEquals(0, req.errorCode, "there should be no error by default");
});
test("Test readyStates", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
var statesVisited = [true, false, false, false, false]
req.onreadystatechange = function() {
statesVisited[req.readyState] = true;
};
req.open("GET", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false);
req.send();
for (var i = 0; i <= req.DONE; i++) {
this.assertEquals(true, statesVisited[i], i + " should be set");
}
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
});
test("Test TEXT request", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
req.open("GET", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false);
req.send();
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
this.assertEquals(200, req.status, "status should be `200`");
this.assertEquals(0, req.errorCode);
this.assertEquals("OK", req.statusText, "statusText should be `OK`");
this.assertNotEquals("", req.getAllResponseHeaders(), "headers should no longer be empty string");
this.assertNull(req.getResponseHeader('invalidheader'), "invalid header should return `null`");
this.assertEquals("GitHub.com", req.getResponseHeader('Server'), "Server header should be GitHub.com");
this.assertEquals('{"id": 1}', req.response);
this.assertEquals('{"id": 1}', req.responseText);
});
test("Test JSON request", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
req.responseType = "json";
req.open("GET", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false);
req.send();
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
this.assertEquals(200, req.status, "status should be `200`");
this.assertEquals(0, req.errorCode);
this.assertEquals("OK", req.statusText, "statusText should be `OK`");
this.assertNotEquals("", req.getAllResponseHeaders(), "headers should no longer be empty string");
this.assertNull(req.getResponseHeader('invalidheader'), "invalid header should return `null`");
this.assertEquals("GitHub.com", req.getResponseHeader('Server'), "Server header should be GitHub.com");
this.assertHasProperty('id', req.response);
this.assertEquals(1, req.response.id);
this.assertEquals('{"id": 1}', req.responseText);
});
test("Test Bad URL", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
req.open("POST", "hifi://domain/path", false);
req.send();
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
this.assertNotEquals(0, req.errorCode);
});
test("Test Bad Method Error", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
req.open("POST", "https://www.google.com", false);
req.send("randomdata");
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
this.assertEquals(405, req.status);
this.assertEquals(202, req.errorCode);
req.open("POST", "https://www.google.com", false)
req.send();
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
this.assertEquals(405, req.status);
this.assertEquals(202, req.errorCode);
});
test("Test abort", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
req.open("POST", "https://www.google.com", true)
req.send();
req.abort();
this.assertEquals(0, req.status);
this.assertEquals(0, req.errorCode);
});
test("Test timeout", function() {
var req = new XMLHttpRequest();
var state = req.readyState;
var timedOut = false;
req.ontimeout = function() {
timedOut = true;
};
req.open("POST", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false)
req.timeout = 1;
req.send();
this.assertEquals(true, timedOut, "request should have timed out");
this.assertEquals(req.DONE, req.readyState, "readyState should be DONE");
this.assertEquals(0, req.status, "status should be `0`");
this.assertEquals(4, req.errorCode, "4 is the timeout error code for QNetworkReply::NetworkError");
});

View file

@ -35,6 +35,7 @@
#include "MenuItemProperties.h"
#include "LocalVoxels.h"
#include "ScriptEngine.h"
#include "XMLHttpRequestClass.h"
VoxelsScriptingInterface ScriptEngine::_voxelsScriptingInterface;
ParticlesScriptingInterface ScriptEngine::_particlesScriptingInterface;
@ -229,6 +230,9 @@ void ScriptEngine::init() {
qScriptRegisterSequenceMetaType<QVector<glm::quat> >(&_engine);
qScriptRegisterSequenceMetaType<QVector<QString> >(&_engine);
QScriptValue xmlHttpRequestConstructorValue = _engine.newFunction(XMLHttpRequestClass::constructor);
_engine.globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue);
QScriptValue printConstructorValue = _engine.newFunction(debugPrint);
_engine.globalObject().setProperty("print", printConstructorValue);

View file

@ -0,0 +1,233 @@
//
// XMLHttpRequestClass.cpp
// libraries/script-engine/src/
//
// Created by Ryan Huffman on 5/2/14.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
// This class is an implementation of the XMLHttpRequest object for scripting use. It provides a near-complete implementation
// of the class described in the Mozilla docs: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QEventLoop>
#include "XMLHttpRequestClass.h"
XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) :
_engine(engine),
_async(true),
_url(),
_method(""),
_responseType(""),
_manager(this),
_request(),
_reply(NULL),
_sendData(NULL),
_rawResponseData(),
_responseData(""),
_onTimeout(QScriptValue::NullValue),
_onReadyStateChange(QScriptValue::NullValue),
_readyState(XMLHttpRequestClass::UNSENT),
_errorCode(QNetworkReply::NoError),
_timeout(0),
_timer(this),
_numRedirects(0) {
_timer.setSingleShot(true);
}
XMLHttpRequestClass::~XMLHttpRequestClass() {
if (_reply) { delete _reply; }
if (_sendData) { delete _sendData; }
}
QScriptValue XMLHttpRequestClass::constructor(QScriptContext* context, QScriptEngine* engine) {
return engine->newQObject(new XMLHttpRequestClass(engine));
}
QScriptValue XMLHttpRequestClass::getStatus() const {
if (_reply) {
return QScriptValue(_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
return QScriptValue(0);
}
QString XMLHttpRequestClass::getStatusText() const {
if (_reply) {
return _reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
}
return "";
}
void XMLHttpRequestClass::abort() {
abortRequest();
}
void XMLHttpRequestClass::setRequestHeader(const QString& name, const QString& value) {
_request.setRawHeader(QByteArray(name.toLatin1()), QByteArray(value.toLatin1()));
}
void XMLHttpRequestClass::requestMetaDataChanged() {
QVariant redirect = _reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
// If this is a redirect, abort the current request and start a new one
if (redirect.isValid() && _numRedirects < MAXIMUM_REDIRECTS) {
_numRedirects++;
abortRequest();
QUrl newUrl = _url.resolved(redirect.toUrl().toString());
_request.setUrl(newUrl);
doSend();
}
}
void XMLHttpRequestClass::requestDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
if (_readyState == OPENED && bytesReceived > 0) {
setReadyState(HEADERS_RECEIVED);
setReadyState(LOADING);
}
}
QScriptValue XMLHttpRequestClass::getAllResponseHeaders() const {
if (_reply) {
QList<QNetworkReply::RawHeaderPair> headerList = _reply->rawHeaderPairs();
QByteArray headers;
for (int i = 0; i < headerList.size(); i++) {
headers.append(headerList[i].first);
headers.append(": ");
headers.append(headerList[i].second);
headers.append("\n");
}
return QString(headers.data());
}
return QScriptValue("");
}
QScriptValue XMLHttpRequestClass::getResponseHeader(const QString& name) const {
if (_reply && _reply->hasRawHeader(name.toLatin1())) {
return QScriptValue(QString(_reply->rawHeader(name.toLatin1())));
}
return QScriptValue::NullValue;
}
void XMLHttpRequestClass::setReadyState(ReadyState readyState) {
if (readyState != _readyState) {
_readyState = readyState;
if (_onReadyStateChange.isFunction()) {
_onReadyStateChange.call(QScriptValue::NullValue);
}
}
}
void XMLHttpRequestClass::open(const QString& method, const QString& url, bool async, const QString& username,
const QString& password) {
if (_readyState == UNSENT) {
_async = async;
_url.setUrl(url);
if (!username.isEmpty()) {
_url.setUserName(username);
}
if (!password.isEmpty()) {
_url.setPassword(password);
}
_request.setUrl(_url);
_method = method;
setReadyState(OPENED);
}
}
void XMLHttpRequestClass::send() {
send(QString::Null());
}
void XMLHttpRequestClass::send(const QString& data) {
if (_readyState == OPENED && !_reply) {
if (!data.isNull()) {
_sendData = new QBuffer(this);
_sendData->setData(data.toUtf8());
}
doSend();
if (!_async) {
QEventLoop loop;
connect(this, SIGNAL(requestComplete()), &loop, SLOT(quit()));
loop.exec();
}
}
}
void XMLHttpRequestClass::doSend() {
_reply = _manager.sendCustomRequest(_request, _method.toLatin1(), _sendData);
connectToReply(_reply);
if (_timeout > 0) {
_timer.start(_timeout);
connect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout()));
}
}
void XMLHttpRequestClass::requestTimeout() {
if (_onTimeout.isFunction()) {
_onTimeout.call(QScriptValue::NullValue);
}
abortRequest();
_errorCode = QNetworkReply::TimeoutError;
setReadyState(DONE);
emit requestComplete();
}
void XMLHttpRequestClass::requestError(QNetworkReply::NetworkError code) {
}
void XMLHttpRequestClass::requestFinished() {
disconnect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout()));
_errorCode = _reply->error();
if (_errorCode == QNetworkReply::NoError) {
_rawResponseData.append(_reply->readAll());
if (_responseType == "json") {
_responseData = _engine->evaluate("(" + QString(_rawResponseData.data()) + ")");
if (_responseData.isError()) {
_engine->clearExceptions();
_responseData = QScriptValue::NullValue;
}
} else if (_responseType == "arraybuffer") {
_responseData = QScriptValue(_rawResponseData.data());
} else {
_responseData = QScriptValue(QString(_rawResponseData.data()));
}
}
setReadyState(DONE);
emit requestComplete();
}
void XMLHttpRequestClass::abortRequest() {
// Disconnect from signals we don't want to receive any longer.
disconnect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout()));
if (_reply) {
disconnectFromReply(_reply);
_reply->abort();
delete _reply;
_reply = NULL;
}
}
void XMLHttpRequestClass::connectToReply(QNetworkReply* reply) {
connect(reply, SIGNAL(finished()), this, SLOT(requestFinished()));
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(requestDownloadProgress(qint64, qint64)));
connect(reply, SIGNAL(metaDataChanged()), this, SLOT(requestMetaDataChanged()));
}
void XMLHttpRequestClass::disconnectFromReply(QNetworkReply* reply) {
disconnect(reply, SIGNAL(finished()), this, SLOT(requestFinished()));
disconnect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
disconnect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(requestDownloadProgress(qint64, qint64)));
disconnect(reply, SIGNAL(metaDataChanged()), this, SLOT(requestMetaDataChanged()));
}

View file

@ -0,0 +1,129 @@
//
// XMLHttpRequestClass.h
// libraries/script-engine/src/
//
// Created by Ryan Huffman on 5/2/14.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_XMLHttpRequestClass_h
#define hifi_XMLHttpRequestClass_h
#include <QBuffer>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QScriptContext>
#include <QScriptEngine>
#include <QScriptValue>
#include <QTimer>
class XMLHttpRequestClass : public QObject {
Q_OBJECT
Q_PROPERTY(QScriptValue response READ getResponse)
Q_PROPERTY(QScriptValue responseText READ getResponseText)
Q_PROPERTY(QString responseType READ getResponseType WRITE setResponseType)
Q_PROPERTY(QScriptValue status READ getStatus)
Q_PROPERTY(QString statusText READ getStatusText)
Q_PROPERTY(QScriptValue readyState READ getReadyState)
Q_PROPERTY(QScriptValue errorCode READ getError)
Q_PROPERTY(int timeout READ getTimeout WRITE setTimeout)
Q_PROPERTY(int UNSENT READ getUnsent)
Q_PROPERTY(int OPENED READ getOpened)
Q_PROPERTY(int HEADERS_RECEIVED READ getHeadersReceived)
Q_PROPERTY(int LOADING READ getLoading)
Q_PROPERTY(int DONE READ getDone)
// Callbacks
Q_PROPERTY(QScriptValue ontimeout READ getOnTimeout WRITE setOnTimeout)
Q_PROPERTY(QScriptValue onreadystatechange READ getOnReadyStateChange WRITE setOnReadyStateChange)
public:
XMLHttpRequestClass(QScriptEngine* engine);
~XMLHttpRequestClass();
static const int MAXIMUM_REDIRECTS = 5;
enum ReadyState {
UNSENT = 0,
OPENED,
HEADERS_RECEIVED,
LOADING,
DONE
};
int getUnsent() const { return UNSENT; };
int getOpened() const { return OPENED; };
int getHeadersReceived() const { return HEADERS_RECEIVED; };
int getLoading() const { return LOADING; };
int getDone() const { return DONE; };
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
int getTimeout() const { return _timeout; }
void setTimeout(int timeout) { _timeout = timeout; }
QScriptValue getResponse() const { return _responseData; }
QScriptValue getResponseText() const { return QScriptValue(QString(_rawResponseData.data())); }
QString getResponseType() const { return _responseType; }
void setResponseType(const QString& responseType) { _responseType = responseType; }
QScriptValue getReadyState() const { return QScriptValue(_readyState); }
QScriptValue getError() const { return QScriptValue(_errorCode); }
QScriptValue getStatus() const;
QString getStatusText() const;
QScriptValue getOnTimeout() const { return _onTimeout; }
void setOnTimeout(QScriptValue function) { _onTimeout = function; }
QScriptValue getOnReadyStateChange() const { return _onReadyStateChange; }
void setOnReadyStateChange(QScriptValue function) { _onReadyStateChange = function; }
public slots:
void abort();
void setRequestHeader(const QString& name, const QString& value);
void open(const QString& method, const QString& url, bool async = true, const QString& username = "",
const QString& password = "");
void send();
void send(const QString& data);
QScriptValue getAllResponseHeaders() const;
QScriptValue getResponseHeader(const QString& name) const;
signals:
void requestComplete();
private:
void setReadyState(ReadyState readyState);
void doSend();
void connectToReply(QNetworkReply* reply);
void disconnectFromReply(QNetworkReply* reply);
void abortRequest();
QScriptEngine* _engine;
bool _async;
QUrl _url;
QString _method;
QString _responseType;
QNetworkAccessManager _manager;
QNetworkRequest _request;
QNetworkReply* _reply;
QBuffer* _sendData;
QByteArray _rawResponseData;
QScriptValue _responseData;
QScriptValue _onTimeout;
QScriptValue _onReadyStateChange;
ReadyState _readyState;
QNetworkReply::NetworkError _errorCode;
int _timeout;
QTimer _timer;
int _numRedirects;
private slots:
void requestFinished();
void requestError(QNetworkReply::NetworkError code);
void requestMetaDataChanged();
void requestDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void requestTimeout();
};
#endif // hifi_XMLHttpRequestClass_h