SparqlController.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.controller;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.service.SparqlService;
import org.entrystore.rest.springboot.util.HttpUtil;
import org.entrystore.rest.springboot.util.SparqlResultFormat;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.nio.charset.StandardCharsets;
@RestController
@RequiredArgsConstructor
public class SparqlController {
private static final int MAX_POST_REQUEST_SIZE = 32 * 1024;
private final SparqlService sparqlService;
@Operation(summary = "Executes a SPARQL tuple query against the public repository")
@GetMapping("/sparql")
public ResponseEntity<StreamingResponseBody> getSparql(
@RequestParam("query") String query,
@RequestParam(value = "format", required = false) String format,
@RequestHeader(value = "Accept", required = false) String acceptHeader) {
return runQuery(query, format, acceptHeader, null);
}
@Operation(summary = "Executes a SPARQL tuple query restricted to a single context's named graph")
@GetMapping("/{context-id}/sparql")
public ResponseEntity<StreamingResponseBody> getContextSparql(
@PathVariable("context-id") String contextId,
@RequestParam("query") String query,
@RequestParam(value = "format", required = false) String format,
@RequestHeader(value = "Accept", required = false) String acceptHeader) {
return runQuery(query, format, acceptHeader, contextId);
}
@Operation(summary = "Executes a SPARQL tuple query (form-encoded POST) against the public repository")
@PostMapping(path = "/sparql", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<StreamingResponseBody> postSparql(
HttpServletRequest request,
@RequestParam("query") String query,
@RequestParam(value = "output", required = false) String output) {
return runFormQuery(request, query, output, null);
}
@Operation(summary = "Executes a SPARQL tuple query (form-encoded POST) restricted to a single context's named graph")
@PostMapping(path = "/{context-id}/sparql", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<StreamingResponseBody> postContextSparql(
HttpServletRequest request,
@PathVariable("context-id") String contextId,
@RequestParam("query") String query,
@RequestParam(value = "output", required = false) String output) {
return runFormQuery(request, query, output, contextId);
}
private ResponseEntity<StreamingResponseBody> runQuery(String query, String format, String acceptHeader, String contextId) {
return buildResponse(SparqlResultFormat.resolve(format, acceptHeader), query, contextId);
}
private ResponseEntity<StreamingResponseBody> runFormQuery(HttpServletRequest request, String query, String output, String contextId) {
// @RequestParam binds from BOTH the form body AND the URL query string; without this guard a client
// could ship a megabyte query in the URL while keeping the body under the 32 KB cap, defeating it.
if (StringUtils.isNotEmpty(request.getQueryString())) {
throw new BadRequestException("POST /sparql parameters must be supplied in the form body, not the URL query string");
}
HttpUtil.checkRequestSize(request, MAX_POST_REQUEST_SIZE);
return buildResponse(SparqlResultFormat.fromOutputForm(output), query, contextId);
}
private ResponseEntity<StreamingResponseBody> buildResponse(SparqlResultFormat format, String query, String contextId) {
StreamingResponseBody body = out -> sparqlService.runQuery(format, query, contextId, out);
return ResponseEntity.ok().contentType(responseMediaType(format)).body(body);
}
private static MediaType responseMediaType(SparqlResultFormat format) {
MediaType mediaType = format.getMediaType();
// RDF4J writes UTF-8 (mandated by SPARQL 1.1 CSV/TSV); without the parameter clients default text/* to ISO-8859-1
return "text".equals(mediaType.getType()) ? new MediaType(mediaType, StandardCharsets.UTF_8) : mediaType;
}
}