Skip to content

Commit

Permalink
Develop RDFox adapter for rest api integration
Browse files Browse the repository at this point in the history
Establish Connection to RDFox REST API
Data Store Management
Data Loading in Turtle Format
SPARQL Query Execution
Error Handling
Unit and Integration Tests
Documentation Updates

Signed-off-by: Haonan Qiu <[email protected]>
  • Loading branch information
q632394 committed Oct 17, 2024
1 parent ffce704 commit 24b871e
Show file tree
Hide file tree
Showing 17 changed files with 617 additions and 24 deletions.
5 changes: 4 additions & 1 deletion cdsp/knowledge-layer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ set(PROJECT_ROOT_DIR "${CMAKE_SOURCE_DIR}")
# Pass the root directory to your C++ code
add_compile_definitions(PROJECT_ROOT="${PROJECT_ROOT_DIR}")

enable_testing()

# Add subdirectory for the websocket-client
add_subdirectory(connector/websocket-client)
add_subdirectory(symbolic-reasoner/rdfox/rdfox-install-test)
add_subdirectory(symbolic-reasoner/rdfox/rdfox-service-test)
add_subdirectory(symbolic-reasoner/rdfox/tests)
10 changes: 8 additions & 2 deletions cdsp/knowledge-layer/connector/utils/data_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ struct ModelConfig {
ReasonerSettings reasoner_settings;
};

struct ServerData {
std::string host;
std::string port;
std::string auth_base64;
};

struct InitConfig {
std::string uuid;
std::string host_websocket_server;
std::string port_websocket_server;
ServerData websocket_server;
ServerData rdfox_server;
std::string oid;
ModelConfig model_config;
};
Expand Down
2 changes: 2 additions & 0 deletions cdsp/knowledge-layer/connector/utils/helper.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "helper.h"

#include <algorithm>

/**
* @brief Converts a given string to lowercase.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Define the WebSocket client target
add_executable(websocket_client main.cpp websocket_client.cpp ../utils/message_utils.cpp ../utils/helper.cpp ../utils/model_config.cpp)
add_executable(
websocket_client main.cpp
websocket_client.cpp
../utils/message_utils.cpp
../utils/helper.cpp
../utils/model_config.cpp
)

# Link libraries
target_link_libraries(websocket_client Boost::system Boost::filesystem Boost::thread)
Expand Down
38 changes: 24 additions & 14 deletions cdsp/knowledge-layer/connector/websocket-client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ std::string DefaultHostWebsocketServer{"127.0.0.1"};
std::string DefaultPortWebSocketServer{"8080"};
std::string ModelConfigurationFile{std::string(PROJECT_ROOT) +
"/symbolic-reasoner/examples/use-case/model/model_config.json"};
std::string DefaultRDFoxServer{"127.0.0.1"};
std::string DefaultPortRDFoxServer{"12110"};
std::string DefaultAuthRDFoxServerBase64{"cm9vdDphZG1pbg=="}; // For 'root:admin' encoded in base64

std::string getEnvVariable(const std::string& envVar, const std::string& defaultValue = "") {
const char* valueEnv = std::getenv(envVar.c_str());
Expand All @@ -34,24 +37,28 @@ void displayHelp() {
std::cout << "This table contains a lists environment variables set for the WebSocket client "
"and their descriptions.\n\n";
// Table header
std::cout << bold << std::left << std::setw(50) << "Variable" << std::setw(50) << "Description"
std::cout << bold << std::left << std::setw(35) << "Variable" << std::setw(65) << "Description"
<< "Value" << reset << "\n";
std::cout << std::string(140, '-') << "\n"; // Line separator

std::cout << std::left << std::setw(50) << "HOST_WEBSOCKET_SERVER" << std::setw(50)
std::cout << std::left << std::setw(35) << "HOST_WEBSOCKET_SERVER" << std::setw(65)
<< "IP address of the WebSocket server"
<< getEnvVariable(" - HOST_WEBSOCKET_SERVER", DefaultHostWebsocketServer) << "\n";
std::cout << std::left << std::setw(50) << "PORT_WEBSOCKET_SERVER" << std::setw(50)
<< getEnvVariable("HOST_WEBSOCKET_SERVER", DefaultHostWebsocketServer) << "\n";
std::cout << std::left << std::setw(35) << "PORT_WEBSOCKET_SERVER" << std::setw(65)
<< "Port number of the WebSocket server"
<< getEnvVariable(" - PORT_WEBSOCKET_SERVER", DefaultPortWebSocketServer) << "\n";
std::cout << std::left << std::setw(50) << "VIN" << std::setw(50)
<< getEnvVariable("PORT_WEBSOCKET_SERVER", DefaultPortWebSocketServer) << "\n";
std::cout << std::left << std::setw(35) << "OBJECT_ID" << std::setw(65)
<< "Object ID to be used in communication"
<< getEnvVariable("VIN", lightRed + "`Not Set (Required)`" + reset) << "\n";
std::cout << std::left << std::setw(50) << "REQUIRED_VSS_DATA_POINTS_FILE" << std::setw(50)
<< "Path to the model configuration file"
<< getEnvVariable(" - REQUIRED_VSS_DATA_POINTS_FILE",
DefaultRequiredVSSDataPointsFile)
<< "\n";
<< getEnvVariable("OBJECT_ID", lightRed + "`Not Set (Required)`" + reset) << "\n";
std::cout << std::left << std::setw(35) << "HOST_RDFOX_SERVER" << std::setw(65)
<< "IP address of the RDFox server"
<< getEnvVariable("HOST_RDFOX_SERVER", DefaultRDFoxServer) << "\n";
std::cout << std::left << std::setw(35) << "PORT_RDFOX_SERVER" << std::setw(65)
<< "Port number of the RDFox server"
<< getEnvVariable("PORT_RDFOX_SERVER", DefaultPortRDFoxServer) << "\n";
std::cout << std::left << std::setw(35) << "AUTH_RDFOX_SERVER_BASE64" << std::setw(65)
<< "Authentication credentials for RDFox Server encoded in base64"
<< getEnvVariable("AUTH_RDFOX_SERVER_BASE64", DefaultAuthRDFoxServerBase64) << "\n";

std::cout << "\n" << bold << "Description:\n" << reset;
std::cout << "This client connects to a WebSocket server and processes incoming messages based "
Expand All @@ -74,13 +81,16 @@ InitConfig AddInitConfig() {
loadModelConfig(ModelConfigurationFile, model_config);

InitConfig init_config;
init_config.host_websocket_server =
init_config.websocket_server.host =
getEnvVariable("HOST_WEBSOCKET_SERVER", DefaultHostWebsocketServer);
init_config.port_websocket_server =
init_config.websocket_server.port =
getEnvVariable("PORT_WEBSOCKET_SERVER", DefaultPortWebSocketServer);
init_config.uuid = boost::uuids::to_string(uuidGenerator());
init_config.oid = getEnvVariable("OBJECT_ID");
init_config.model_config = model_config;
init_config.rdfox_server.host = getEnvVariable("HOST_RDFOX_SERVER");
init_config.rdfox_server.port = getEnvVariable("PORT_RDFOX_SERVER");
init_config.rdfox_server.auth_base64 = getEnvVariable("AUTH_RDFOX_SERVER_BASE64");
return init_config;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ WebSocketClient::WebSocketClient(const InitConfig& init_config)

void WebSocketClient::run() {
resolver_.async_resolve(
init_config_.host_websocket_server, init_config_.port_websocket_server,
init_config_.websocket_server.host, init_config_.websocket_server.port,
beast::bind_front_handler(&WebSocketClient::onResolve, shared_from_this()));

// Run the io_context to process asynchronous events
Expand All @@ -52,15 +52,15 @@ void WebSocketClient::onConnect(boost::system::error_code ec,
return Fail(ec, "Connection failed:");
}

ws_.async_handshake(init_config_.host_websocket_server, "/",
ws_.async_handshake(init_config_.websocket_server.host, "/",
beast::bind_front_handler(&WebSocketClient::handshake, shared_from_this()));
}

void WebSocketClient::handshake(beast::error_code ec) {
if (ec) {
return Fail(ec, "Handshake failed:");
}
std::cout << "Connected to WebSocket server: " << init_config_.host_websocket_server
std::cout << "Connected to WebSocket server: " << init_config_.websocket_server.host
<< std::endl
<< std::endl;

Expand Down
6 changes: 5 additions & 1 deletion cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ This folder contains the necessary files to interact with RDFox, a high-performa

For instructions on how to start the RDFox service required for this project, see [this guide](../../../../docker/README.md#rdfox-restful-api).

### RDFox Adapter

See how to interact with the RDFox server using the [RDFox adapter](./src/README.md#rdfoxadapter).

### Getting Started

This project includes a small C++ application to verify that the RDFox service has been configured and started correctly. After compiling the project, you should be able to run the application from [`./rdfox-install-test/rdfox_test_main.cpp`](./rdfox-install-test/rdfox_test_main.cpp). The RDFox Test executable will be generated in the `/cdsp/knowledge-layer/build/bin/` directory. You can run it with the following command:
This project includes a small C++ application to verify that the RDFox service has been configured and started correctly. After compiling the project, you should be able to run the application from [`./rdfox-service-test/rdfox_test_main.cpp`](./rdfox-service-test/rdfox_test_main.cpp). The RDFox Test executable will be generated in the `/cdsp/knowledge-layer/build/bin/` directory. You can run it with the following command:

```bash
$ ./websocket_client
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Define the RDFox test target
add_executable(rdfox_test rdfox_test_main.cpp)
add_executable(
rdfox_test rdfox_test_main.cpp
)

# Link Boost libraries
target_link_libraries(rdfox_test Boost::system Boost::filesystem Boost::thread)
Expand Down
25 changes: 25 additions & 0 deletions cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# RDFoxAdapter

The RDFoxAdapter class provides an interface for interacting with an RDFox server over HTTP. It enables operations like checking the existence of a datastore, creating a datastore, loading data in Turtle format, querying using SPARQL, and deleting a datastore.

> [!NOTE] Data store
> When the RDFoxAdapter initializes creates (if is does not exists) a datastore called `vehicle_ds` in the RDFox server.
## Features

- **Initialize a Datastore:** Checks if a specified datastore exists on the RDFox server. If not, it creates the datastore.
- **Load Data:** Loads Turtle data into a specified datastore.
- **Query Data:** Executes SPARQL queries against the RDFox datastore and retrieves results.
- **Delete Datastore:** Removes a specified datastore from the RDFox server.
- **Send HTTP Requests:** Supports sending GET and POST HTTP requests to interact with RDFox.

## Methods

- **initialize():** Ensures the datastore is present; creates it if missing.
- **loadData(const std::string& ttl_data):** Loads Turtle data into the datastore.
- **queryData(const std::string& sparql_query):** Executes a SPARQL query and returns the result.
- **deleteDataStore():** Deletes the datastore if it exists.

> [!NOTE] See test
> - [../tests/test_rdfox_adapter_integration.cpp](../tests/test_rdfox_adapter_integration.cpp)
> - [../tests/test_rdfox_adapter_unit.cpp](../tests/test_rdfox_adapter_unit.cpp)
179 changes: 179 additions & 0 deletions cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#include "rdfox-adapter.h"

#include <iostream>

RDFoxAdapter::RDFoxAdapter(const std::string& host, const std::string& port,
const std::string& auth_base64,
const std::string& data_store = "vehicle_ds")
: host_(host),
port_(port),
auth_header_base64_("Basic " + auth_base64),
data_store_(data_store) {
std::cout << "Initializing RDFox adapter..." << std::endl;
}

/**
* @brief Initializes the RDFoxAdapter by ensuring the data store is created.
*
* This method checks if the data store specified by `data_store_` exists.
* If the data store exists, it logs a message indicating its existence.
* If the data store does not exist, it attempts to create it by sending
* a POST request to the appropriate endpoint. If the creation is successful,
* a success message is logged. Otherwise, an exception is thrown.
*
* @throws std::runtime_error if the data store creation fails.
*/
void RDFoxAdapter::initialize() {
// checks if the data store exists, create it if not
if (checkDataStore()) {
std::cout << "Data store '" + data_store_ + "' is already created." << std::endl;
} else {
std::cout << "Data store '" << data_store_ << "' does not exist. Creating it..."
<< std::endl;
// Creates a data store
std::string target = "/datastores/" + data_store_;
if (sendPostRequest(target, "", "application/json")) {
std::cout << "Data store '" << data_store_ << "' created successfully." << std::endl;
} else {
throw std::runtime_error("Failed to create datastore '" + data_store_ + "'");
}
}
}

/**
* Loads Turtle data into the RDFox datastore.
*
* @param ttl_data A string containing the Turtle data to be loaded into the datastore.
* @return A boolean value indicating whether the data was successfully loaded.
*/
bool RDFoxAdapter::loadData(const std::string& ttl_data) {
std::string target = "/datastores/" + data_store_ + "/content";
return sendPostRequest(target, ttl_data, "text/turtle") ? true : false;
};

/**
* Executes a SPARQL query against the RDFox datastore.
*
* @param sparql_query The SPARQL query string to be executed.
* @return The response from the datastore as a string if the query is successful,
* otherwise an empty string.
*/
std::string RDFoxAdapter::queryData(const std::string& sparql_query) {
std::string target = "/datastores/" + data_store_ + "/sparql";
auto response = sendPostRequest(target, sparql_query, "application/sparql-query");
return response.has_value() ? response.value() : "";
}

bool RDFoxAdapter::deleteDataStore() {
if (checkDataStore()) {
std::string target = "/datastores/" + data_store_;
std::string responseBody;
if (sendRequest(http::verb::delete_, target, "", "", "", responseBody)) {
std::cout << "Data store '" + data_store_ + "' have been removed successfully."
<< std::endl;
} else {
std::cout << "Data store '" + data_store_ + "' could not be removed." << std::endl;
return false;
}
} else {
std::cout << "Data store '" + data_store_ + "' does not exists anymore." << std::endl;
}
return true;
}

/**
* @brief Checks if the data store exists on the server.
*
* @return true if the data store is found in the server's response; false otherwise.
*/
bool RDFoxAdapter::checkDataStore() {
std::string target = "/datastores";
std::string response = sendGetRequest(target, "text/csv; charset=UTF-8");
if (response.find(data_store_) != std::string::npos) {
return true;
} else {
return false;
}
}

std::optional<std::string> RDFoxAdapter::sendPostRequest(const std::string& target,
const std::string& body,
const std::string& contentType) {
std::string responseBody;
if (sendRequest(http::verb::post, target, body, contentType, "", responseBody)) {
return responseBody;
}
return std::nullopt;
}

std::string RDFoxAdapter::sendGetRequest(const std::string& target, const std::string& acceptType) {
std::string responseBody;
if (sendRequest(http::verb::get, target, "", "", acceptType, responseBody)) {
return responseBody;
}
return "";
}

/**
* Sends an HTTP request to a specified target and retrieves the response.
*
* @param method The HTTP method to use for the request (e.g., GET, POST).
* @param target The target URI for the request.
* @param body The body content to send with the request.
* @param contentType The MIME type of the body content.
* @param acceptType The MIME type that the client is willing to accept in the response.
* @param responseBody A reference to a string where the response body will be stored.
* @return True if the request was successful and the response status is OK, Created, or No Content;
* false otherwise.
*/
bool RDFoxAdapter::sendRequest(http::verb method, const std::string& target,
const std::string& body, const std::string& contentType,
const std::string& acceptType, std::string& responseBody) {
net::io_context ioc;
tcp::resolver resolver(ioc);
tcp::socket socket(ioc);

try {
// Resolve and connect to the host
auto const results = resolver.resolve(host_, port_);
net::connect(socket, results.begin(), results.end());

// Set up the HTTP request
http::request<http::string_body> req{method, target, 11};
req.set(http::field::host, host_);
req.set(http::field::authorization, auth_header_base64_);
if (!contentType.empty()) {
req.set(http::field::content_type, contentType);
}
if (!acceptType.empty()) {
req.set(http::field::accept, acceptType);
}
req.body() = body;
req.prepare_payload();

// Send the request and receive the response
http::write(socket, req);
beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(socket, buffer, res);

responseBody = res.body();

if (res.result() != http::status::ok && res.result() != http::status::created &&
res.result() != http::status::no_content) {
std::cerr << createErrorMessage(res.body(), res.result_int()) << std::endl;
return false;
}
return true;
} catch (const beast::system_error& e) {
std::cerr << "Network error: " << e.what() << std::endl;
return false;
} catch (const std::exception& e) {
std::cerr << "Error in request: " << e.what() << std::endl;
return false;
}
}

std::string RDFoxAdapter::createErrorMessage(const std::string& error_msg, int error_code) {
return error_msg + " (Code: " + std::to_string(error_code) + ")";
}
Loading

0 comments on commit 24b871e

Please sign in to comment.