Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Accept Headers Using Antlr. #521

Merged
merged 12 commits into from
Dec 15, 2021
85 changes: 71 additions & 14 deletions src/engine/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,22 +287,79 @@ boost::asio::awaitable<void> Server::processQuery(
qet.recursivelySetTimeoutTimer(timeoutTimer);
LOG(TRACE) << qet.asString() << std::endl;

using ad_utility::MediaType;
// Determine the result media type
std::vector<MediaType> supportedMediaTypes{
ad_utility::MediaType::sparqlJson, ad_utility::MediaType::tsv,
ad_utility::MediaType::csv, ad_utility::MediaType::qleverJson};

std::string_view acceptHeader = request.base()[http::field::accept];
std::optional<MediaType> mediaType =
ad_utility::getMediaTypeFromAcceptHeader(acceptHeader,
supportedMediaTypes);

// TODO<joka921> Remove this hack, as soon as the QLever UI sends proper
// accept headers.
if (acceptHeader.empty()) {
mediaType = MediaType::qleverJson;
}

if (!mediaType.has_value()) {
co_return co_await send(
createBadRequestResponse("Did not find any supported media type "
"in this \'Accept:\' header field: " +
std::string{acceptHeader},
request));
}

// TODO<joka921> make our own UIs use accept headers and then remove this
// code.
if (containsParam("action", "csv_export")) {
// CSV export
auto responseGenerator = composeResponseSepValues(pq, qet, ',');
auto response = createOkResponse(std::move(responseGenerator), request,
ad_utility::MediaType::csv);
co_await send(std::move(response));
mediaType = ad_utility::MediaType::csv;
} else if (containsParam("action", "tsv_export")) {
// TSV export
auto responseGenerator = composeResponseSepValues(pq, qet, '\t');
auto response = createOkResponse(std::move(responseGenerator), request,
ad_utility::MediaType::tsv);
co_await send(std::move(response));
} else {
// Normal case: JSON response
auto responseString = composeResponseJson(pq, qet, requestTimer, maxSend);
co_await sendJson(std::move(responseString));
mediaType = ad_utility::MediaType::tsv;
}

AD_CHECK(mediaType.has_value());
switch (mediaType.value()) {
case ad_utility::MediaType::csv: {
auto responseGenerator = composeResponseSepValues(pq, qet, ',');
auto response = createOkResponse(std::move(responseGenerator), request,
ad_utility::MediaType::csv);
co_await send(std::move(response));
} break;
case ad_utility::MediaType::tsv: {
auto responseGenerator = composeResponseSepValues(pq, qet, '\t');
auto response = createOkResponse(std::move(responseGenerator), request,
ad_utility::MediaType::tsv);
co_await send(std::move(response));
} break;
case ad_utility::MediaType::qleverJson: {
// Normal case: JSON response
auto responseString =
composeResponseJson(pq, qet, requestTimer, maxSend);
co_await sendJson(std::move(responseString));
} break;
case ad_utility::MediaType::sparqlJson: {
// TODO<joka921> implement this, it is in a different PR which needs
// only a bit of polishing.
auto errorString =
"This endpoint currently only supports tsv, csv, and a custom "
"non-standard json format. Support for standard "
"application/sparql-results+json export will be implemented soon";
co_return co_await send(createBadRequestResponse(errorString, request));

// TODO<joka921> This will be the code when the other PR is merged.
/*
auto responseString =
composeResponseSparqlJson(pq, qet, requestTimer, maxSend);
co_await sendJson(std::move(responseString));
*/
}
default:
// This should never happen, because we have carefully restricted the
// subset of mediaTypes that can occur here.
AD_CHECK(false);
}
// Print the runtime info. This needs to be done after the query
// was computed.
Expand Down
8 changes: 6 additions & 2 deletions src/util/HttpServer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
add_library(httpServer HttpServer.h HttpUtils.h UrlParser.h UrlParser.cpp MediaTypes.h MediaTypes.cpp)
target_link_libraries(httpServer ${ICU_LIBRARIES})
add_subdirectory(HttpParser)
add_library(mediaTypes MediaTypes.h MediaTypes.cpp)
target_link_libraries(mediaTypes ${ICU_LIBRARIES} absl::flat_hash_map)
add_library(httpServer HttpServer.h HttpUtils.h UrlParser.h UrlParser.cpp "HttpParser/AcceptHeaderQleverVisitor.h")

target_link_libraries(httpServer mediaTypes httpParser ${ICU_LIBRARIES})
5 changes: 5 additions & 0 deletions src/util/HttpServer/HttpParser/AcceptHeaderQleverVisitor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2021, University of Freiburg,
// Chair of Algorithms and Data Structures.
// Author: Johannes Kalmbach <kalmbach@cs.uni-freiburg.de>

#include "./AcceptHeaderQleverVisitor.h"
179 changes: 179 additions & 0 deletions src/util/HttpServer/HttpParser/AcceptHeaderQleverVisitor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2021, University of Freiburg,
// Chair of Algorithms and Data Structures.
// Author: Johannes Kalmbach <kalmbach@cs.uni-freiburg.de>

#ifndef QLEVER_ACCEPTHEADERQLEVERVISITOR_H
#define QLEVER_ACCEPTHEADERQLEVERVISITOR_H

// Generated from AcceptHeader.g4 by ANTLR 4.9.2

#include "../../Exception.h"
#include "../MediaTypes.h"
#include "./generated/AcceptHeaderVisitor.h"
#include "antlr4-runtime.h"

/**
* /brief Visitor class for the ANTLR-based Accept header parser. Main
* entrypoint is the `visitAccept` or `visitAcceptWithEof` function, which
* yields an `antlrcpp::any` that holds a
* `std::vector<ad_utility::MediaTypeWithQuality>`.
*/
class AcceptHeaderQleverVisitor : public AcceptHeaderVisitor {
public:
using MediaType = ad_utility::MediaType;

class Exception : public std::runtime_error {
using std::runtime_error::runtime_error;
};
class ParseException : public std::runtime_error {
using std::runtime_error::runtime_error;
};
class NotSupportedException : public std::exception {
public:
explicit NotSupportedException(std::string_view featureName)
: _message{"The feature \"" + std::string{featureName} +
"\" is currently not supported by this parser"} {}
[[nodiscard]] const char* what() const noexcept override {
return _message.c_str();
}

private:
std::string _message;
};

// ________________________________________________________________________
antlrcpp::Any visitAcceptWithEof(
AcceptHeaderParser::AcceptWithEofContext* ctx) override {
return ctx->accept()->accept(this);
}

// ________________________________________________________________________
antlrcpp::Any visitAccept(AcceptHeaderParser::AcceptContext* ctx) override {
std::vector<ad_utility::MediaTypeWithQuality> acceptedMediaTypes;
for (const auto& child : ctx->rangeAndParams()) {
auto mediaType =
child->accept(this)
.as<std::optional<ad_utility::MediaTypeWithQuality>>();
if (mediaType.has_value()) {
acceptedMediaTypes.push_back(mediaType.value());
}
if (acceptedMediaTypes.empty()) {
throw Exception{
"Not a single media type known to this parser was detected in \"" +
ctx->getText() + '\"'};
}
}
return {std::move(acceptedMediaTypes)};
}

antlrcpp::Any visitRangeAndParams(
AcceptHeaderParser::RangeAndParamsContext* ctx) override {
std::optional<ad_utility::MediaTypeWithQuality> result = std::nullopt;
float quality = 1.0;
if (ctx->acceptParams()) {
quality = ctx->acceptParams()->accept(this).as<float>();
}
using V = std::optional<ad_utility::MediaTypeWithQuality::Variant>;
auto mediaRange = ctx->mediaRange()->accept(this).as<V>();
if (mediaRange.has_value()) {
result.emplace(ad_utility::MediaTypeWithQuality{
quality, std::move(mediaRange.value())});
}
return result;
}

antlrcpp::Any visitMediaRange(
AcceptHeaderParser::MediaRangeContext* ctx) override {
using V = std::optional<ad_utility::MediaTypeWithQuality::Variant>;
if (!ctx->subtype()) {
if (!ctx->type()) {
return V{ad_utility::MediaTypeWithQuality::Wildcard{}};
} else {
return V{ad_utility::MediaTypeWithQuality::TypeWithWildcard{
ctx->type()->getText()}};
}
}
if (!ctx->parameter().empty()) {
throw NotSupportedException{
"Media type parameters, e.g. \"charset=...\""};
}
return V{ad_utility::toMediaType(ctx->getText())};
}

antlrcpp::Any visitType(AcceptHeaderParser::TypeContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitSubtype(AcceptHeaderParser::SubtypeContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitAcceptParams(
AcceptHeaderParser::AcceptParamsContext* ctx) override {
if (!ctx->acceptExt().empty()) {
throw NotSupportedException{"Media type parameters like \"charset=...\""};
}
return ctx->weight()->accept(this);
}

antlrcpp::Any visitWeight(AcceptHeaderParser::WeightContext* ctx) override {
auto throwException = [ctx] {
throw ParseException{
"Decimal values for quality parameters in accept header must be "
"between 0 and 1, and must have at most 3 decimal digits. Found "
"illegal quality value " +
ctx->qvalue()->getText()};
};
if (ctx->qvalue()->getText().size() > 5) {
throwException();
}
auto quality = std::stof(ctx->qvalue()->getText());
if (quality > 1.0f) {
throwException();
}
return quality;
}

antlrcpp::Any visitQvalue(AcceptHeaderParser::QvalueContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitAcceptExt(
AcceptHeaderParser::AcceptExtContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitParameter(
AcceptHeaderParser::ParameterContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitToken(AcceptHeaderParser::TokenContext* ctx) override {
return ctx->getText();
return visitChildren(ctx);
}

antlrcpp::Any visitTchar(AcceptHeaderParser::TcharContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitQuotedString(
AcceptHeaderParser::QuotedStringContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}

antlrcpp::Any visitQuoted_pair(
AcceptHeaderParser::Quoted_pairContext* ctx) override {
AD_CHECK(false); // Should be unreachable.
return visitChildren(ctx);
}
};

#endif // QLEVER_ACCEPTHEADERQLEVERVISITOR_H
7 changes: 7 additions & 0 deletions src/util/HttpServer/HttpParser/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
add_library(httpParser AcceptHeaderQleverVisitor.h AcceptHeaderQleverVisitor.cpp
generated/AcceptHeaderBaseListener.h generated/AcceptHeaderBaseListener.cpp
generated/AcceptHeaderLexer.h generated/AcceptHeaderLexer.cpp
generated/AcceptHeaderListener.h generated/AcceptHeaderListener.cpp
generated/AcceptHeaderParser.h generated/AcceptHeaderParser.cpp
generated/AcceptHeaderVisitor.h generated/AcceptHeaderVisitor.cpp)
target_link_libraries(httpParser antlr4_static)