1212
1313import static javax .servlet .http .HttpServletResponse .SC_OK ;
1414
15- import java .io .ByteArrayOutputStream ;
15+ import java .io .IOException ;
1616import java .io .OutputStream ;
1717import java .nio .charset .Charset ;
1818import java .util .Map ;
19+ import java .util .Objects ;
1920
2021import javax .servlet .http .HttpServletRequest ;
2122import javax .servlet .http .HttpServletResponse ;
2425import org .eclipse .rdf4j .http .server .repository .RepositoryInterceptor ;
2526import org .eclipse .rdf4j .model .IRI ;
2627import org .eclipse .rdf4j .model .Resource ;
28+ import org .eclipse .rdf4j .model .Statement ;
2729import org .eclipse .rdf4j .model .Value ;
2830import org .eclipse .rdf4j .repository .RepositoryConnection ;
2931import org .eclipse .rdf4j .repository .RepositoryException ;
3032import org .eclipse .rdf4j .rio .RDFFormat ;
33+ import org .eclipse .rdf4j .rio .RDFHandler ;
3134import org .eclipse .rdf4j .rio .RDFHandlerException ;
3235import org .eclipse .rdf4j .rio .RDFWriter ;
3336import org .eclipse .rdf4j .rio .RDFWriterFactory ;
37+ import org .slf4j .Logger ;
38+ import org .slf4j .LoggerFactory ;
3439import 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 */
4246public 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