ExportStatementsView.java
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.http.server.repository.statements;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rdf4j.http.server.ServerHTTPException;
import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFHandler;
import org.eclipse.rdf4j.rio.RDFHandlerException;
import org.eclipse.rdf4j.rio.RDFWriter;
import org.eclipse.rdf4j.rio.RDFWriterFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.View;
/**
* Streams statements as RDF in the format requested by the client.
*
* @author Herko ter Horst
*/
public class ExportStatementsView implements View {
private static final Logger logger = LoggerFactory.getLogger(ExportStatementsView.class);
public static final String SUBJECT_KEY = "subject";
public static final String PREDICATE_KEY = "predicate";
public static final String OBJECT_KEY = "object";
public static final String CONTEXTS_KEY = "contexts";
public static final String USE_INFERENCING_KEY = "useInferencing";
public static final String CONNECTION_KEY = "connection";
public static final String TRANSACTION_ID_KEY = "transactionID";
public static final String FACTORY_KEY = "factory";
public static final String HEADERS_ONLY = "headersOnly";
private static final ExportStatementsView INSTANCE = new ExportStatementsView();
public static int MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS;
static {
int max = 1024; // default value
String maxStatements = System.getProperty(
"org.eclipse.rdf4j.http.server.repository.statements.ExportStatementsView.MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS");
if (maxStatements != null) {
try {
int userMax = Integer.parseInt(maxStatements);
if (userMax >= -1) {
max = userMax;
} else {
logger.warn(
"Invalid value for MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS: {}, must be >= -1, using default value of {}.",
maxStatements, max);
}
} catch (NumberFormatException e) {
logger.warn("Invalid value for MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS: "
+ maxStatements, e);
}
}
MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS = max;
}
public static ExportStatementsView getInstance() {
return INSTANCE;
}
private ExportStatementsView() {
}
@Override
public String getContentType() {
// Spring ignores this for View implementations; we set it in render().
return null;
}
@Override
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setBufferSize(1024 * 1024); // 1MB
Resource subj = (Resource) Objects.requireNonNull(model, "model should not be null").get(SUBJECT_KEY);
IRI pred = (IRI) model.get(PREDICATE_KEY);
Value obj = (Value) model.get(OBJECT_KEY);
Resource[] contexts = (Resource[]) model.get(CONTEXTS_KEY);
boolean useInferencing = Boolean.TRUE.equals(model.get(USE_INFERENCING_KEY));
boolean headersOnly = Boolean.TRUE.equals(model.get(HEADERS_ONLY));
RDFWriterFactory factory = (RDFWriterFactory) model.get(FACTORY_KEY);
RDFFormat rdfFormat = factory.getRDFFormat();
attemptToDetectExceptions(request, factory, headersOnly, subj, pred, obj, useInferencing, contexts);
response.setStatus(SC_OK);
String mimeType = rdfFormat.getDefaultMIMEType();
if (rdfFormat.hasCharset()) {
Charset charset = rdfFormat.getCharset();
mimeType += "; charset=" + charset.name();
}
response.setContentType(mimeType);
String filename = "statements";
if (rdfFormat.getDefaultFileExtension() != null) {
filename += "." + rdfFormat.getDefaultFileExtension();
}
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
if (headersOnly) {
response.setContentLength(0);
response.flushBuffer();
return;
}
try (OutputStream out = response.getOutputStream()) {
RDFWriter writer = factory.getWriter(out);
try (RepositoryConnection conn = RepositoryInterceptor.getRepositoryConnection(request)) {
conn.exportStatements(subj, pred, obj, useInferencing, writer, contexts);
out.flush();
response.flushBuffer();
} catch (RDFHandlerException e) {
var serverHTTPException = new ServerHTTPException("Serialization error: " + e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
}
throw serverHTTPException;
} catch (RepositoryException e) {
var serverHTTPException = new ServerHTTPException("Repository error: " + e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
}
throw serverHTTPException;
} catch (Throwable e) {
if (!response.isCommitted()) {
response.reset();
}
throw e;
}
}
}
private static void attemptToDetectExceptions(HttpServletRequest request, RDFWriterFactory rdfWriterFactory,
boolean headersOnly, Resource subj, IRI pred, Value obj, boolean useInferencing, Resource[] contexts)
throws IOException, ServerHTTPException {
if (MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS == 0) {
return;
}
try (OutputStream out = OutputStream.nullOutputStream()) {
RDFHandler rdfWriter = new LimitedSizeRDFHandler(rdfWriterFactory.getWriter(out),
MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS);
if (!headersOnly) {
try (RepositoryConnection conn = RepositoryInterceptor.getRepositoryConnection(request)) {
conn.exportStatements(subj, pred, obj, useInferencing, rdfWriter, contexts);
} catch (RDFHandlerException e) {
throw new ServerHTTPException("Serialization error: " + e.getMessage(), e);
} catch (RepositoryException e) {
throw new ServerHTTPException("Repository error: " + e.getMessage(), e);
} catch (LimitedSizeReachedException ignored) {
}
}
}
}
private static class LimitedSizeRDFHandler implements RDFHandler {
private final RDFHandler delegate;
private final long maxSize;
private long currentSize = 0;
public LimitedSizeRDFHandler(RDFHandler delegate, long maxSize) {
this.delegate = delegate;
this.maxSize = maxSize;
}
@Override
public void startRDF() throws RDFHandlerException {
delegate.startRDF();
}
@Override
public void endRDF() throws RDFHandlerException {
delegate.endRDF();
}
@Override
public void handleNamespace(String prefix, String uri) throws RDFHandlerException {
delegate.handleNamespace(prefix, uri);
incrementCurrentSize();
}
@Override
public void handleStatement(Statement st) throws RDFHandlerException {
delegate.handleStatement(st);
incrementCurrentSize();
}
@Override
public void handleComment(String comment) throws RDFHandlerException {
delegate.handleComment(comment);
incrementCurrentSize();
}
private void incrementCurrentSize() {
currentSize++;
if (maxSize >= 0 && currentSize > maxSize) {
endRDF();
logger.trace(
"Limited size reached, throwing LimitedSizeReachedException to signal that we are done testing the export of statements for exceptions.");
throw new LimitedSizeReachedException();
}
}
}
private static class LimitedSizeReachedException extends RuntimeException {
@Override
public Throwable fillInStackTrace() {
// Do not fill in the stack trace to avoid performance overhead
return this;
}
}
}