This commit is contained in:
Matthias Janetschek
2015-05-26 17:13:51 +02:00
parent 519073ebab
commit 6031718d0e
4 changed files with 327 additions and 175 deletions
@@ -11,18 +11,26 @@ namespace service {
namespace webserver {
namespace resources {
using namespace httpserver;
using namespace ::libvariant;
using namespace libvariant;
class ResourceException : public std::runtime_error {
/**
* Used for internal error handling.
*/
class ResourceException
: public std::runtime_error {
protected:
int errorCode;
public:
ResourceException(std::string msg, int errorCode)
: std::runtime_error(msg), errorCode(errorCode) {}
int getErrorCode() { return errorCode; }
ResourceException(const std::string& msg, int errorCode)
: std::runtime_error(msg), errorCode(errorCode) {}
int getErrorCode() {
return errorCode;
}
};
@@ -32,169 +40,249 @@ void DevicesHTTPResource::init( const std::vector<std::string>& prefixPath,
const std::string& absolutePath,
httpserver::webserver& webserver,
WebServerService& service ) {
this->prefixPath = prefixPath;
disallow_all();
set_allowing(MHD_HTTP_METHOD_GET, true);
set_allowing(MHD_HTTP_METHOD_POST, true);
webserver.register_resource(absolutePath, this, true);
webResource.prefixPath = prefixPath;
webResource.disallow_all();
webResource.set_allowing(MHD_HTTP_METHOD_GET, true);
webResource.set_allowing(MHD_HTTP_METHOD_POST, true);
webserver.register_resource(absolutePath, &webResource, true);
}
void DevicesHTTPResource::render_GET(const httpserver::http_request& req, httpserver::http_response** res) {
DevicesHTTPResource::WebResource::WebResource(devicerepository::DeviceRepositoryService* repoService)
: repoService(repoService) {
infoCategoryFuncs.insert({"type", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant {
return device->getDeviceType();
} });
infoCategoryFuncs.insert({"status_topic", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant {
return device->getStatusTopic();
} });
infoCategoryFuncs.insert({"command_topic", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant {
return device->getCmdTopic();
} });
infoCategoryFuncs.insert({"current_state", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant {
return device->getCurrentState();
} });
infoCategoryFuncs.insert({"last_state", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant {
return device->getLastState();
} });
/*infoCategoryFuncs.insert({"url", [this](devicerepository::Device* device, const std::string& category, const httpserver::http_request& req)->Variant {
std::stringstream ss;
req.
ss << http
return device->getLastState();
} });*/
}
void DevicesHTTPResource::WebResource::render_GET(const httpserver::http_request& req, httpserver::http_response** res) {
std::cout << "DevicesHTTPResource::render_GET()" << std::endl;
std::cout << req;
try {
if (req.get_path_pieces_size() < prefixPath.size() + 1) {
ROS_ERROR_STREAM("Requested path " << req.get_path() << " has fewer pieces than prefix path.");
throw ResourceException("Internal Server Error", 500);
} else {
bool isSetCommand = false;
Variant setValue;
if (req.get_arg("command").compare("set") == 0) {
isSetCommand = true;
std::string value = req.get_arg("value");
if (value.empty()) {
throw ResourceException("No value given for set command.", 400);
} else {
boost::algorithm::trim(value);
if (boost::starts_with(value, "{") || boost::starts_with(value, "[")) {
setValue = DeserializeJSON(value);
} else {
setValue = Variant(value);
}
// Get method parameter
DataMethod dataMethod = DataMethod::GET; // Default method is get
std::string methodValue;
std::string methodRaw = req.get_arg("method");
if (!methodRaw.empty()) {
if (methodRaw.compare("get") == 0) {
// do nothing
} else if (methodRaw.compare("command") == 0) {
dataMethod = DataMethod::COMMAND;
methodValue = req.get_arg("value");
if (methodValue.empty()) {
throw ResourceException((boost::format("Method parameter \"%1%\" requires a value parameter.") % methodRaw).str(), 400);
}
}
if (req.get_path_pieces_size() == prefixPath.size() + 1) {
if (isSetCommand) {
throw ResourceException("No device given for set command.", 400);
} else if (methodRaw.compare("statusupdate") == 0) {
dataMethod = DataMethod::STATUSUPDATE;
methodValue = req.get_arg("value");
if (methodValue.empty()) {
throw ResourceException((boost::format("Method parameter \"%1%\" requires a value parameter.") % methodRaw).str(), 400);
}
Variant result;
for (auto d : repoService->getDevices()) {
result[d.lock()->getDeviceID()] = getDeviceInfo(d.lock().get());
}
*res = new http_response(http_response_builder(SerializeJSON(result, true), 200).string_response());
} else {
std::string devid = req.get_path_piece(prefixPath.size() + 2);
auto device = repoService->getDevice(devid);
if (!device.expired()) {
if (isSetCommand) {
std::vector<std::string> path;
for (int i = prefixPath.size() + 2; i < req.get_path_pieces_size(); i++) {
path.push_back(req.get_path_piece(i));
}
try {
setDeviceState(device.lock().get(), setValue, path);
*res = new http_response(http_response_builder("OK", 200).string_response());
} catch (std::exception& e) {
auto msg = (boost::format("ERROR: %1%") % e.what()).str();
*res = new http_response(http_response_builder(msg , 400).string_response());
}
} else {
std::string info;
if (req.get_path_pieces_size() > prefixPath.size() + 2) {
info = req.get_path_piece(prefixPath.size() + 2);
}
std::stringstream ss;
for (int i = prefixPath.size() + 3; i < req.get_path_pieces_size(); i++) {
ss << "/" << req.get_path_piece(i);
}
std::string path = ss.str();
Variant result = getDeviceInfo(device.lock().get(), info, path);
*res = new http_response(http_response_builder(SerializeJSON(result, true), 200).string_response());
}
} else {
throw ResourceException((boost::format("Could not find device \"%1%\"") % devid).str(), 404);
}
throw ResourceException((boost::format("Unknown method parameter \"%1%\"") % methodRaw).str(), 400);
}
}
} catch (ResourceException& e) {
*res = new http_response(http_response_builder(e.what(), e.getErrorCode()).string_response());
} catch (std::exception& e) {
*res = new http_response(http_response_builder(e.what(), 500).string_response());
}
// No device given
if (req.get_path_pieces_size() <= prefixPath.size() + 1) {
if (dataMethod != DataMethod::GET) {
throw ResourceException("Requested method requires a device.", 400);
}
Variant result;
for (auto d : repoService->getDevices()) {
auto dev = d.lock();
result[dev->getDeviceID()] = getDeviceInfo(dev.get(), req);
}
*res = new http_response(http_response_builder(SerializeJSON(result, true), 200).string_response());
// Device given
} else {
std::string devid = req.get_path_piece(prefixPath.size() + 1);
auto device = repoService->getDevice(devid);
if (!device.expired()) {
switch (dataMethod) {
case DataMethod::GET:
{
std::string category = req.get_path_piece(prefixPath.size() + 2);
std::vector<std::string> path;
for (int i = prefixPath.size() + 3; i < req.get_path_pieces_size(); i++) {
path.push_back(req.get_path_piece(i));
}
Variant result = getDeviceInfo(device.lock().get(), req, category, path);
std::string msg;
switch (getDataFormat(req)) {
case DataFormat::JSON:
msg = SerializeJSON(result, true);
break;
case DataFormat::JSONP:
{
std::string padding = req.get_arg("callback");
if (padding.empty()) {
padding = req.get_arg("jsonp");
}
if (padding.empty()) {
throw ResourceException("No callback parameter specified for JSONP.", 500);
}
std::stringstream ss;
ss << padding << "(" << SerializeJSON(result, false) << ")";
msg = ss.str();
}
break;
case DataFormat::YAML:
msg = SerializeYAML(result);
break;
case DataFormat::XMLPLIST:
msg = SerializeXMLPLIST(result, true);
break;
case DataFormat::BUNDLEHDR:
msg = SerializeBundleHdr(result);
break;
case DataFormat::MSGPACK:
msg = Serialize(result, SERIALIZE_MSGPACK);
break;
default:
throw ResourceException("Internal server error", 500);
}
*res = new http_response(http_response_builder(msg, 200).string_response());
}
break;
case DataMethod::COMMAND:
{
std::vector<std::string> path;
for (int i = prefixPath.size() + 2; i < req.get_path_pieces_size(); i++) {
path.push_back(req.get_path_piece(i));
}
sendDeviceCommand(device.lock().get(), req, methodValue, path);
*res = new http_response(http_response_builder("OK: Command send.", 200).string_response());
}
break;
case DataMethod::STATUSUPDATE:
throw ResourceException("ToDo: Implement method.", 500);
default:
throw ResourceException("Internal server error", 500);
}
} else {
throw ResourceException((boost::format("Could not find device \"%1%\"") % devid).str(), 404);
}
}
} catch (ResourceException& e) {
*res = new http_response(http_response_builder((boost::format("ERROR: %1%") % e.what()).str(), e.getErrorCode()).string_response());
} catch (std::exception& e) {
*res = new http_response(http_response_builder((boost::format("ERROR: %1%") % e.what()).str(), 500).string_response());
}
std::cout << **res;
}
Variant DevicesHTTPResource::getDeviceInfo(devicerepository::Device* device, const std::string& info, const std::string& path) {
Variant retval;
if (info.empty()) {
retval["type"] = device->getDeviceType();
retval["status_topic"] = device->getStatusTopic();
retval["command_topic"] = device->getCmdTopic();
retval["current_state"] = device->getCurrentState();
retval["last_state"] = device->getLastState();
} else {
if (info.compare("type") == 0) {
if (path.empty()) {
retval = device->getDeviceType();
} else {
throw ResourceException("Device info \"type\" has no path.", 400);
}
} else if (info.compare("status_topic") == 0) {
if (path.empty()) {
retval = device->getStatusTopic();
} else {
throw ResourceException("Device info \"status_topic\" has no path.", 400);
}
} else if (info.compare("command_topic") == 0) {
if (path.empty()) {
retval = device->getCmdTopic();
} else {
throw ResourceException("Device info \"command_topic\" has no path.", 400);
}
} else if (info.compare("current_state") == 0) {
retval = device->getCurrentState();
if (!path.empty()) {
if (retval.HasPath(path)) {
retval = retval.GetPath(path);
} else {
throw ResourceException((boost::format("Unknown path \"%1%\" for device info \"current_state\".") % path).str(), 404);
}
}
} else if (info.compare("last_state") == 0) {
retval = device->getLastState();
if (!path.empty()) {
if (retval.HasPath(path)) {
retval = retval.GetPath(path);
} else {
throw ResourceException((boost::format("Unknown path \"%1%\" for device info \"last_state\".") % path).str(), 404);
}
}
void DevicesHTTPResource::WebResource::render_POST(const httpserver::http_request& req, httpserver::http_response** res) {
std::cout << "DevicesHTTPResource::render_POST()" << std::endl;
std::cout << req;
std::cout << "content: " << req.get_content() << std::endl;
*res = new http_response(http_response_builder("ToDo: Implement POST", 500).string_response());
std::cout << **res;
}
DevicesHTTPResource::WebResource::DataFormat DevicesHTTPResource::WebResource::getDataFormat(const httpserver::http_request& req) {
DataFormat retval = DataFormat::JSON;
std::string formatRaw = req.get_arg("format");
if (!formatRaw.empty()) {
if (formatRaw.compare("json") == 0) {
// do nothing
} else if (formatRaw.compare("jsonp") == 0) {
retval = DataFormat::JSONP;
} else if (formatRaw.compare("yaml") == 0) {
retval = DataFormat::YAML;
} else if (formatRaw.compare("xmlplist") == 0) {
retval = DataFormat::XMLPLIST;
} else if (formatRaw.compare("bundlehdr") == 0) {
retval = DataFormat::BUNDLEHDR;
} else if (formatRaw.compare("msgpack") == 0) {
retval = DataFormat::MSGPACK;
} else {
throw ResourceException((boost::format("Unknown device info \"%1%\"") % info).str(), 404);
throw ResourceException((boost::format("Unknown format parameter \"%1%\"") % formatRaw).str(), 400);
}
}
return retval;
}
void DevicesHTTPResource::setDeviceState(devicerepository::Device* device, const Variant& cmd, std::vector<std::string> path) {
Variant cmdMsg;
if (path.size() > 0) {
Variant& current = cmdMsg;
for (int i = 0; i < path.size()-1; i++) {
Variant tmp;
current[path[i]] = tmp;
current = tmp;
Variant DevicesHTTPResource::WebResource::getDeviceInfo(devicerepository::Device* device, const httpserver::http_request& req, const std::string& category, const std::vector<std::string>& path) {
Variant retval;
// We want all info categories
if (category.empty()) {
for (auto c : infoCategoryFuncs) {
retval[c.first] = c.second(device, req, category);
}
current[path[path.size()-1]] = cmd;
// We want a single info category
} else {
cmdMsg = cmd;
auto c = infoCategoryFuncs.find(category);
if (c != infoCategoryFuncs.end()) {
retval = c->second(device, req, category);
if (path.size() > 0) {
std::stringstream ss;
for (auto p : path) {
ss << "/" << p;
}
std::string pathstr = ss.str();
if (retval.HasPath(pathstr)) {
retval = retval.GetPath(pathstr);
} else {
throw ResourceException((boost::format("Unknown path \"%1%\" for device info category \"%2%\".") % pathstr % category).str(), 404);
}
}
} else {
throw ResourceException((boost::format("Unknown device info category \"%1%\"") % category).str(), 404);
}
}
device->sendCommand(cmdMsg);
return retval;
}
void DevicesHTTPResource::render_POST(const httpserver::http_request& req, httpserver::http_response** res) {
std::cout << "DevicesHTTPResource::render_POST()" << std::endl;
std::cout << req;
*res = new http_response(http_response_builder("ToDo: Implement POST", 500).string_response());
std::cout << **res;
void DevicesHTTPResource::WebResource::sendDeviceCommand(devicerepository::Device* device, const httpserver::http_request& req, const std::string& cmd, const std::vector<std::string>& path) {
Variant cmdMsg;
VariantRef mPart = cmdMsg.Ref();
if (path.size() > 0) {
for (int i = 0; i < path.size(); i++) {
mPart[path[i]] = Variant();
mPart.ReassignRef(mPart[path[i]]);
}
}
/*
* The libvariant json deserializer knows nothing about RFC 7159, therefore non-object or non-array values are rejected.
* As a workaround we use the yaml deserializer (yaml can be considered a super-set of json).
* Fun fact: the json serializer accepts non-object or non-array values.
*/
//mPart = DeserializeJSON(cmd);
mPart = DeserializeYAML(cmd);
device->sendCommand(cmdMsg);
}
@@ -8,6 +8,7 @@
#include <ros/ros.h>
#include <httpserver.hpp>
#include <string>
#include <functional>
@@ -19,19 +20,77 @@ namespace webserver {
namespace resources {
/**
* Implements a REST API for accessing the device repository and getting info about a device.
*
* HTTP GET REQUESTS:
* 1. http://[prefixPath]/devices - Returns all known devices
* 2. http://[prefixPath]/devices/[deviceID] - Returns complete info about the given device as JSON object.
* 3. http://[prefixPath]/devices/[deviceID]/[infoCategory]/[infoPath] - Returns selected info about the given device as JSON object ([infoCategory]/[infoPath] is treated as JSON Path selecting parts of the complete JSON info object from 1).
* 4. http://[prefixPath]/devices/[deviceID]/[cmdPath]?method=command&value=[cmdValue] - Sends the command [cmdPath]/[cmdValue] to the given device.
* 5. http://[prefixPath]/devices/[deviceID]/[statusPath]?method=statusupdate&value=[statusValue] - Sends the status update [statusPath]/[statusValue] to the given device (Only useful for virtual devices).
*
* HTTP POST REQUESTS (content-type: application/x-www-form-urlencoded):
* - Basically the same as 4 and 5 from the HTTP GET REQUESTS list, but parameters are transmitted in the request body instead of the URL.
*
* HTTP POST REQUESTS (content-type: application/json):
* - http://[prefixPath]/devices/[deviceID]/[method]/[methodPath] - Semantically the same as 4 and 5 from the HTTP GET REQUESTS list, but the method parameter is part of the URL and the request body only contains a single JSON text.
*
*
* A note about JSON object returned by the above URLs: According to RFC 4627 a JSON text is a serialized object or array.
* This was later relaxed by RFC 7159 which defines that a JSON text is a serialized value. Our API adheres to RFC 7159 and may return
* JSON texts (namely single strings, numbers or boolean values) that are not valid JSON texts according to RFC 4627.
* Keep in mind that there are still a lot of JSON libraries and validators out there which still no nothing about RFC 7159.
*/
class DevicesHTTPResource
: public virtual webserver::WebResource,
public httpserver::http_resource<DevicesHTTPResource> {
: public virtual webserver::WebResource {
private:
devicerepository::DeviceRepositoryService* repoService = nullptr;
std::vector<std::string> prefixPath;
/**
* libhttpserver does a static_cast with any class registered as http resource. Unfortunately this breaks classes
* using virtual inheritance. Therefore we register this inner class which does not use virtual inheritance.
*/
class WebResource
: public httpserver::http_resource<WebResource> {
public:
enum class DataMethod { GET, COMMAND, STATUSUPDATE };
enum class DataFormat { JSON, JSONP, YAML, XMLPLIST, BUNDLEHDR, MSGPACK };
typedef std::function<libvariant::Variant( devicerepository::Device* device,
const httpserver::http_request& req,
const std::string& category )> infoCategoryFunc_t;
devicerepository::DeviceRepositoryService* repoService = nullptr;
std::vector<std::string> prefixPath;
std::map<std::string, infoCategoryFunc_t> infoCategoryFuncs;
WebResource(devicerepository::DeviceRepositoryService* repoService);
void render_GET(const httpserver::http_request& req, httpserver::http_response** res);
void render_POST(const httpserver::http_request& req, httpserver::http_response** res);
DataFormat getDataFormat(const httpserver::http_request& req);
libvariant::Variant getDeviceInfo( devicerepository::Device* device,
const httpserver::http_request& req,
const std::string& category = std::string(),
const std::vector<std::string>& path = std::vector<std::string>() );
void sendDeviceCommand( devicerepository::Device* device,
const httpserver::http_request& req,
const std::string& cmd,
const std::vector<std::string>& path = std::vector<std::string>() );
} webResource;
public:
DevicesHTTPResource(devicerepository::DeviceRepositoryService* repoService)
: repoService(repoService) {}
: webResource(repoService) {}
virtual void init( const std::vector<std::string>& prefixPath,
const std::string& registeredPath,
@@ -39,16 +98,6 @@ public:
httpserver::webserver& webserver,
webserver::WebServerService& service );
void render_GET(const httpserver::http_request& req, httpserver::http_response** res);
void render_POST(const httpserver::http_request& req, httpserver::http_response** res);
libvariant::Variant getDeviceInfo( devicerepository::Device* device,
const std::string& info = std::string(),
const std::string& path = std::string() );
void setDeviceState( devicerepository::Device* device,
const libvariant::Variant& cmd,
std::vector<std::string> path = std::vector<std::string>() );
};
} // namespace resources
@@ -23,30 +23,31 @@ void SchemaHTTPResource::init( const std::vector<std::string>& prefixPath,
const std::string& absolutePath,
httpserver::webserver& webserver,
WebServerService& service ) {
this->prefixPath = prefixPath;
disallow_all();
set_allowing(MHD_HTTP_METHOD_GET, true);
webserver.register_resource(absolutePath, this, true);
webResource.prefixPath = prefixPath;
webResource.disallow_all();
webResource.set_allowing(MHD_HTTP_METHOD_GET, true);
webserver.register_resource(absolutePath, &webResource, true);
}
void SchemaHTTPResource::render_GET(const httpserver::http_request& req, httpserver::http_response** res) {
if (req.get_path_pieces_size() < prefixPath.size() + 1) {
ROS_ERROR_STREAM("Requested path " << req.get_path() << " has fewer pieces than prefix path.");
*res = new http_response(http_response_builder("Internal Server Error", 500).string_response());
} else if (req.get_path_pieces_size() == prefixPath.size() + 1) {
*res = new http_response(http_response_builder("No schema given", 400).string_response());
void SchemaHTTPResource::WebResource::render_GET(const httpserver::http_request& req, httpserver::http_response** res) {
// Requested URL is too short to contain any schema
if (req.get_path_pieces_size() <= prefixPath.size() + 1) {
*res = new http_response(http_response_builder("ERROR: No schema given", 400).string_response());
// Requested URL may contain a valid schema
} else {
// Get requested schema
std::stringstream ss;
for (int i = prefixPath.size() + 1; i < req.get_path_pieces_size(); i++) {
ss << "/" << req.get_path_piece(i);
}
auto schemaPath = (boost::format("hos:schema:/%1%") % ss.str()).str();
// Try to retrieve schema
try {
auto schema = schemaLoader->GetSchema(schemaPath);
*res = new http_response(http_response_builder(SerializeJSON(schema, true), 200).string_response());
} catch(std::exception& e) {
*res = new http_response(http_response_builder((boost::format("Could not find schema \"%1%\": %2%") % schemaPath % e.what()).str(), 404).string_response());
*res = new http_response(http_response_builder((boost::format("ERROR: Could not find schema \"%1%\": %2%") % schemaPath % e.what()).str(), 404).string_response());
}
}
}
@@ -20,25 +20,39 @@ namespace resources {
class SchemaHTTPResource
: public virtual webserver::WebResource,
public httpserver::http_resource<SchemaHTTPResource> {
: public virtual webserver::WebResource {
private:
ros::NodeHandle* nodehandle = nullptr;
utils::SchemaLoader* schemaLoader = nullptr;
std::vector<std::string> prefixPath;
/**
* libhttpserver does a static_cast with any class registered as http resource. Unfortunately this breaks classes
* using virtual inheritance. Therefore we register this inner class which does not use virtual inheritance.
*/
class WebResource
: public httpserver::http_resource<WebResource> {
public:
ros::NodeHandle* nodehandle = nullptr;
utils::SchemaLoader* schemaLoader = nullptr;
std::vector<std::string> prefixPath;
WebResource(ros::NodeHandle* nodehandle, utils::SchemaLoader* schemaLoader)
: nodehandle(nodehandle), schemaLoader(schemaLoader) {}
void render_GET(const httpserver::http_request& req, httpserver::http_response** res);
} webResource;
public:
SchemaHTTPResource(ros::NodeHandle* nodehandle, utils::SchemaLoader* schemaLoader)
: nodehandle(nodehandle), schemaLoader(schemaLoader) {}
: webResource(nodehandle, schemaLoader) {}
virtual void init( const std::vector<std::string>& prefixPath,
const std::string& registeredPath,
const std::string& absolutePath,
httpserver::webserver& webserver,
WebServerService& service );
void render_GET(const httpserver::http_request& req, httpserver::http_response** res);
};
} // namespace resources