Skip to content

Commit 158af44

Browse files
authored
GH-5333 Allow exporting of data directly to the http output stream (#5334)
2 parents f6a72a9 + eb9381b commit 158af44

File tree

6 files changed

+345
-42
lines changed

6 files changed

+345
-42
lines changed

docker/Dockerfile-jetty

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ WORKDIR /tmp
1111
RUN unzip -q /tmp/rdf4j.zip
1212

1313
# Final workbench
14-
FROM jetty:9-jre17-eclipse-temurin
14+
FROM jetty:9-jre17-eclipse-temurin
1515
LABEL org.opencontainers.image.authors="Bart Hanssens ([email protected])"
1616

1717
USER root
1818

19-
ENV JAVA_OPTIONS="-Dorg.eclipse.rdf4j.appdata.basedir=/var/rdf4j -Dorg.eclipse.rdf4j.rio.jsonld_secure_mode=false"
19+
ENV JAVA_OPTIONS="-Xmx2g -Dorg.eclipse.rdf4j.appdata.basedir=/var/rdf4j -Dorg.eclipse.rdf4j.rio.jsonld_secure_mode=false"
2020
ENV JETTY_MODULES="server,bytebufferpool,threadpool,security,servlet,webapp,ext,plus,deploy,annotations,http,jsp,jstl"
2121

2222
COPY --from=temp /tmp/eclipse-rdf4j*/war/*.war /var/lib/jetty/webapps/

scripts/milestone-release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ git checkout main
218218
RELEASE_NOTES_BRANCH="${MVN_VERSION_RELEASE}-release-notes"
219219
git checkout -b "${RELEASE_NOTES_BRANCH}"
220220

221-
tar -cvzf "site/static/javadoc/${MVN_VERSION_RELEASE}.tgz" -C target/site/apidocs .
221+
tar --no-xattrs --exclude ".*" -cvzf "site/static/javadoc/${MVN_VERSION_RELEASE}.tgz" -C target/site/apidocs .
222222
git add --all
223223
git commit -s -a -m "javadocs for ${MVN_VERSION_RELEASE}"
224224
git push --set-upstream origin "${RELEASE_NOTES_BRANCH}"

scripts/release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ mvn package -Passembly -DskipTests -Djapicmp.skip
259259
git checkout main
260260
git checkout -b "${RELEASE_NOTES_BRANCH}"
261261

262-
tar -cvzf "site/static/javadoc/${MVN_VERSION_RELEASE}.tgz" -C target/site/apidocs .
262+
tar --no-xattrs --exclude ".*" -cvzf "site/static/javadoc/${MVN_VERSION_RELEASE}.tgz" -C target/site/apidocs .
263263
cp -f "site/static/javadoc/${MVN_VERSION_RELEASE}.tgz" "site/static/javadoc/latest.tgz"
264264
git add --all
265265
git commit -s -a -m "javadocs for ${MVN_VERSION_RELEASE}"

tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/statements/ExportStatementsView.java

Lines changed: 157 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212

1313
import static javax.servlet.http.HttpServletResponse.SC_OK;
1414

15-
import java.io.ByteArrayOutputStream;
15+
import java.io.IOException;
1616
import java.io.OutputStream;
1717
import java.nio.charset.Charset;
1818
import java.util.Map;
19+
import java.util.Objects;
1920

2021
import javax.servlet.http.HttpServletRequest;
2122
import javax.servlet.http.HttpServletResponse;
@@ -24,42 +25,62 @@
2425
import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor;
2526
import org.eclipse.rdf4j.model.IRI;
2627
import org.eclipse.rdf4j.model.Resource;
28+
import org.eclipse.rdf4j.model.Statement;
2729
import org.eclipse.rdf4j.model.Value;
2830
import org.eclipse.rdf4j.repository.RepositoryConnection;
2931
import org.eclipse.rdf4j.repository.RepositoryException;
3032
import org.eclipse.rdf4j.rio.RDFFormat;
33+
import org.eclipse.rdf4j.rio.RDFHandler;
3134
import org.eclipse.rdf4j.rio.RDFHandlerException;
3235
import org.eclipse.rdf4j.rio.RDFWriter;
3336
import org.eclipse.rdf4j.rio.RDFWriterFactory;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
3439
import org.springframework.web.servlet.View;
3540

3641
/**
37-
* View used to export statements. Renders the statements as RDF using a serialization specified using a parameter or
38-
* Accept header.
42+
* Streams statements as RDF in the format requested by the client.
3943
*
4044
* @author Herko ter Horst
4145
*/
4246
public class ExportStatementsView implements View {
4347

44-
public static final String SUBJECT_KEY = "subject";
48+
private static final Logger logger = LoggerFactory.getLogger(ExportStatementsView.class);
4549

50+
public static final String SUBJECT_KEY = "subject";
4651
public static final String PREDICATE_KEY = "predicate";
47-
4852
public static final String OBJECT_KEY = "object";
49-
5053
public static final String CONTEXTS_KEY = "contexts";
51-
5254
public static final String USE_INFERENCING_KEY = "useInferencing";
53-
5455
public static final String CONNECTION_KEY = "connection";
55-
5656
public static final String TRANSACTION_ID_KEY = "transactionID";
57-
5857
public static final String FACTORY_KEY = "factory";
59-
6058
public static final String HEADERS_ONLY = "headersOnly";
6159

6260
private static final ExportStatementsView INSTANCE = new ExportStatementsView();
61+
public static int MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS;
62+
63+
static {
64+
int max = 1024; // default value
65+
String maxStatements = System.getProperty(
66+
"org.eclipse.rdf4j.http.server.repository.statements.ExportStatementsView.MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS");
67+
if (maxStatements != null) {
68+
try {
69+
int userMax = Integer.parseInt(maxStatements);
70+
if (userMax >= -1) {
71+
max = userMax;
72+
} else {
73+
logger.warn(
74+
"Invalid value for MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS: {}, must be >= -1, using default value of {}.",
75+
maxStatements, max);
76+
}
77+
} catch (NumberFormatException e) {
78+
logger.warn("Invalid value for MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS: "
79+
+ maxStatements, e);
80+
}
81+
}
82+
MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS = max;
83+
}
6384

6485
public static ExportStatementsView getInstance() {
6586
return INSTANCE;
@@ -70,53 +91,156 @@ private ExportStatementsView() {
7091

7192
@Override
7293
public String getContentType() {
94+
// Spring ignores this for View implementations; we set it in render().
7395
return null;
7496
}
7597

76-
@SuppressWarnings("rawtypes")
7798
@Override
7899
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
79-
Resource subj = (Resource) model.get(SUBJECT_KEY);
100+
101+
response.setBufferSize(1024 * 1024); // 1MB
102+
103+
Resource subj = (Resource) Objects.requireNonNull(model, "model should not be null").get(SUBJECT_KEY);
80104
IRI pred = (IRI) model.get(PREDICATE_KEY);
81105
Value obj = (Value) model.get(OBJECT_KEY);
82106
Resource[] contexts = (Resource[]) model.get(CONTEXTS_KEY);
83-
boolean useInferencing = (Boolean) model.get(USE_INFERENCING_KEY);
107+
boolean useInferencing = Boolean.TRUE.equals(model.get(USE_INFERENCING_KEY));
108+
boolean headersOnly = Boolean.TRUE.equals(model.get(HEADERS_ONLY));
109+
110+
RDFWriterFactory factory = (RDFWriterFactory) model.get(FACTORY_KEY);
111+
RDFFormat rdfFormat = factory.getRDFFormat();
112+
113+
attemptToDetectExceptions(request, factory, headersOnly, subj, pred, obj, useInferencing, contexts);
114+
115+
response.setStatus(SC_OK);
116+
117+
String mimeType = rdfFormat.getDefaultMIMEType();
118+
if (rdfFormat.hasCharset()) {
119+
Charset charset = rdfFormat.getCharset();
120+
mimeType += "; charset=" + charset.name();
121+
}
122+
response.setContentType(mimeType);
123+
124+
String filename = "statements";
125+
if (rdfFormat.getDefaultFileExtension() != null) {
126+
filename += "." + rdfFormat.getDefaultFileExtension();
127+
}
128+
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
129+
130+
if (headersOnly) {
131+
response.setContentLength(0);
132+
response.flushBuffer();
133+
return;
134+
}
135+
136+
try (OutputStream out = response.getOutputStream()) {
137+
RDFWriter writer = factory.getWriter(out);
138+
try (RepositoryConnection conn = RepositoryInterceptor.getRepositoryConnection(request)) {
139+
conn.exportStatements(subj, pred, obj, useInferencing, writer, contexts);
140+
out.flush();
141+
response.flushBuffer();
142+
} catch (RDFHandlerException e) {
143+
var serverHTTPException = new ServerHTTPException("Serialization error: " + e.getMessage(), e);
144+
if (!response.isCommitted()) {
145+
response.reset();
146+
}
147+
throw serverHTTPException;
148+
} catch (RepositoryException e) {
149+
var serverHTTPException = new ServerHTTPException("Repository error: " + e.getMessage(), e);
150+
if (!response.isCommitted()) {
151+
response.reset();
152+
}
153+
throw serverHTTPException;
154+
} catch (Throwable e) {
155+
if (!response.isCommitted()) {
156+
response.reset();
157+
}
158+
throw e;
159+
}
84160

85-
boolean headersOnly = (Boolean) model.get(HEADERS_ONLY);
161+
}
86162

87-
RDFWriterFactory rdfWriterFactory = (RDFWriterFactory) model.get(FACTORY_KEY);
163+
}
88164

89-
RDFFormat rdfFormat = rdfWriterFactory.getRDFFormat();
165+
private static void attemptToDetectExceptions(HttpServletRequest request, RDFWriterFactory rdfWriterFactory,
166+
boolean headersOnly, Resource subj, IRI pred, Value obj, boolean useInferencing, Resource[] contexts)
167+
throws IOException, ServerHTTPException {
168+
if (MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS == 0) {
169+
return;
170+
}
90171

91-
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
92-
RDFWriter rdfWriter = rdfWriterFactory.getWriter(baos);
172+
try (OutputStream out = OutputStream.nullOutputStream()) {
173+
RDFHandler rdfWriter = new LimitedSizeRDFHandler(rdfWriterFactory.getWriter(out),
174+
MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS);
93175
if (!headersOnly) {
94176
try (RepositoryConnection conn = RepositoryInterceptor.getRepositoryConnection(request)) {
95177
conn.exportStatements(subj, pred, obj, useInferencing, rdfWriter, contexts);
96178
} catch (RDFHandlerException e) {
97179
throw new ServerHTTPException("Serialization error: " + e.getMessage(), e);
98180
} catch (RepositoryException e) {
99181
throw new ServerHTTPException("Repository error: " + e.getMessage(), e);
182+
} catch (LimitedSizeReachedException ignored) {
100183
}
101184
}
102-
try (OutputStream out = response.getOutputStream()) {
103-
response.setStatus(SC_OK);
185+
}
186+
}
104187

105-
String mimeType = rdfFormat.getDefaultMIMEType();
106-
if (rdfFormat.hasCharset()) {
107-
Charset charset = rdfFormat.getCharset();
108-
mimeType += "; charset=" + charset.name();
109-
}
110-
response.setContentType(mimeType);
188+
private static class LimitedSizeRDFHandler implements RDFHandler {
111189

112-
String filename = "statements";
113-
if (rdfFormat.getDefaultFileExtension() != null) {
114-
filename += "." + rdfFormat.getDefaultFileExtension();
115-
}
116-
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
117-
out.write(baos.toByteArray());
190+
private final RDFHandler delegate;
191+
private final long maxSize;
192+
private long currentSize = 0;
193+
194+
public LimitedSizeRDFHandler(RDFHandler delegate, long maxSize) {
195+
this.delegate = delegate;
196+
this.maxSize = maxSize;
197+
}
198+
199+
@Override
200+
public void startRDF() throws RDFHandlerException {
201+
delegate.startRDF();
202+
}
203+
204+
@Override
205+
public void endRDF() throws RDFHandlerException {
206+
delegate.endRDF();
207+
}
208+
209+
@Override
210+
public void handleNamespace(String prefix, String uri) throws RDFHandlerException {
211+
delegate.handleNamespace(prefix, uri);
212+
incrementCurrentSize();
213+
}
214+
215+
@Override
216+
public void handleStatement(Statement st) throws RDFHandlerException {
217+
delegate.handleStatement(st);
218+
incrementCurrentSize();
219+
}
220+
221+
@Override
222+
public void handleComment(String comment) throws RDFHandlerException {
223+
delegate.handleComment(comment);
224+
incrementCurrentSize();
225+
}
226+
227+
private void incrementCurrentSize() {
228+
currentSize++;
229+
if (maxSize >= 0 && currentSize > maxSize) {
230+
endRDF();
231+
logger.trace(
232+
"Limited size reached, throwing LimitedSizeReachedException to signal that we are done testing the export of statements for exceptions.");
233+
throw new LimitedSizeReachedException();
118234
}
119235
}
120236
}
121237

238+
private static class LimitedSizeReachedException extends RuntimeException {
239+
@Override
240+
public Throwable fillInStackTrace() {
241+
// Do not fill in the stack trace to avoid performance overhead
242+
return this;
243+
}
244+
}
245+
122246
}

0 commit comments

Comments
 (0)