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
4448 public static final String SUBJECT_KEY = "subject" ;
45-
4649 public static final String PREDICATE_KEY = "predicate" ;
47-
4850 public static final String OBJECT_KEY = "object" ;
49-
5051 public static final String CONTEXTS_KEY = "contexts" ;
51-
5252 public static final String USE_INFERENCING_KEY = "useInferencing" ;
53-
5453 public static final String CONNECTION_KEY = "connection" ;
55-
5654 public static final String TRANSACTION_ID_KEY = "transactionID" ;
57-
5855 public static final String FACTORY_KEY = "factory" ;
59-
6056 public static final String HEADERS_ONLY = "headersOnly" ;
6157
6258 private static final ExportStatementsView INSTANCE = new ExportStatementsView ();
59+ public static final int MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS = 1024 ;
60+
61+ private static final Logger logger = LoggerFactory .getLogger (ExportStatementsView .class );
6362
6463 public static ExportStatementsView getInstance () {
6564 return INSTANCE ;
@@ -70,53 +69,152 @@ private ExportStatementsView() {
7069
7170 @ Override
7271 public String getContentType () {
72+ // Spring ignores this for View implementations; we set it in render().
7373 return null ;
7474 }
7575
76- @ SuppressWarnings ("rawtypes" )
7776 @ Override
7877 public void render (Map model , HttpServletRequest request , HttpServletResponse response ) throws Exception {
79- Resource subj = (Resource ) model .get (SUBJECT_KEY );
78+
79+ response .setBufferSize (1024 * 1024 ); // 1MB
80+
81+ Resource subj = (Resource ) Objects .requireNonNull (model , "model should not be null" ).get (SUBJECT_KEY );
8082 IRI pred = (IRI ) model .get (PREDICATE_KEY );
8183 Value obj = (Value ) model .get (OBJECT_KEY );
8284 Resource [] contexts = (Resource []) model .get (CONTEXTS_KEY );
83- boolean useInferencing = (Boolean ) model .get (USE_INFERENCING_KEY );
85+ boolean useInferencing = Boolean .TRUE .equals (model .get (USE_INFERENCING_KEY ));
86+ boolean headersOnly = Boolean .TRUE .equals (model .get (HEADERS_ONLY ));
87+
88+ RDFWriterFactory factory = (RDFWriterFactory ) model .get (FACTORY_KEY );
89+ RDFFormat rdfFormat = factory .getRDFFormat ();
8490
85- boolean headersOnly = (Boolean ) model .get (HEADERS_ONLY );
91+ attemptToDetectExceptions (request , factory , headersOnly , subj , pred , obj , useInferencing , contexts );
92+
93+ response .setStatus (SC_OK );
94+
95+ String mimeType = rdfFormat .getDefaultMIMEType ();
96+ if (rdfFormat .hasCharset ()) {
97+ Charset charset = rdfFormat .getCharset ();
98+ mimeType += "; charset=" + charset .name ();
99+ }
100+ response .setContentType (mimeType );
101+
102+ String filename = "statements" ;
103+ if (rdfFormat .getDefaultFileExtension () != null ) {
104+ filename += "." + rdfFormat .getDefaultFileExtension ();
105+ }
106+ response .setHeader ("Content-Disposition" , "attachment; filename=" + filename );
107+
108+ if (headersOnly ) {
109+ response .setContentLength (0 );
110+ response .flushBuffer ();
111+ return ;
112+ }
113+
114+ try (OutputStream out = response .getOutputStream ()) {
115+ RDFWriter writer = factory .getWriter (out );
116+ try (RepositoryConnection conn = RepositoryInterceptor .getRepositoryConnection (request )) {
117+ conn .exportStatements (subj , pred , obj , useInferencing , writer , contexts );
118+ out .flush ();
119+ response .flushBuffer ();
120+ } catch (RDFHandlerException e ) {
121+ var serverHTTPException = new ServerHTTPException ("Serialization error: " + e .getMessage (), e );
122+ if (!response .isCommitted ()) {
123+ response .reset ();
124+ }
125+ throw serverHTTPException ;
126+ } catch (RepositoryException e ) {
127+ var serverHTTPException = new ServerHTTPException ("Repository error: " + e .getMessage (), e );
128+ if (!response .isCommitted ()) {
129+ response .reset ();
130+ }
131+ throw serverHTTPException ;
132+ } catch (Throwable e ) {
133+ if (!response .isCommitted ()) {
134+ response .reset ();
135+ }
136+ throw e ;
137+ }
86138
87- RDFWriterFactory rdfWriterFactory = ( RDFWriterFactory ) model . get ( FACTORY_KEY );
139+ }
88140
89- RDFFormat rdfFormat = rdfWriterFactory . getRDFFormat ();
141+ }
90142
91- try (ByteArrayOutputStream baos = new ByteArrayOutputStream ()) {
92- RDFWriter rdfWriter = rdfWriterFactory .getWriter (baos );
143+ private static void attemptToDetectExceptions (HttpServletRequest request , RDFWriterFactory rdfWriterFactory ,
144+ boolean headersOnly , Resource subj , IRI pred , Value obj , boolean useInferencing , Resource [] contexts )
145+ throws IOException , ServerHTTPException {
146+ try (OutputStream out = OutputStream .nullOutputStream ()) {
147+ RDFHandler rdfWriter = new LimitedSizeRDFHandler (rdfWriterFactory .getWriter (out ),
148+ MAX_NUMBER_OF_STATEMENTS_WHEN_TESTING_FOR_POSSIBLE_EXCEPTIONS );
93149 if (!headersOnly ) {
94150 try (RepositoryConnection conn = RepositoryInterceptor .getRepositoryConnection (request )) {
95151 conn .exportStatements (subj , pred , obj , useInferencing , rdfWriter , contexts );
96152 } catch (RDFHandlerException e ) {
97153 throw new ServerHTTPException ("Serialization error: " + e .getMessage (), e );
98154 } catch (RepositoryException e ) {
99155 throw new ServerHTTPException ("Repository error: " + e .getMessage (), e );
156+ } catch (LimitedSizeReachedException ignored ) {
100157 }
101158 }
102- try ( OutputStream out = response . getOutputStream ()) {
103- response . setStatus ( SC_OK );
159+ }
160+ }
104161
105- String mimeType = rdfFormat .getDefaultMIMEType ();
106- if (rdfFormat .hasCharset ()) {
107- Charset charset = rdfFormat .getCharset ();
108- mimeType += "; charset=" + charset .name ();
109- }
110- response .setContentType (mimeType );
162+ private static class LimitedSizeRDFHandler implements RDFHandler {
111163
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 ());
164+ private final RDFHandler delegate ;
165+ private final long maxSize ;
166+ private long currentSize = 0 ;
167+
168+ public LimitedSizeRDFHandler (RDFHandler delegate , long maxSize ) {
169+ this .delegate = delegate ;
170+ this .maxSize = maxSize ;
171+ }
172+
173+ @ Override
174+ public void startRDF () throws RDFHandlerException {
175+ delegate .startRDF ();
176+ }
177+
178+ @ Override
179+ public void endRDF () throws RDFHandlerException {
180+ delegate .endRDF ();
181+ }
182+
183+ @ Override
184+ public void handleNamespace (String prefix , String uri ) throws RDFHandlerException {
185+ delegate .handleNamespace (prefix , uri );
186+ incrementCurrentSize ();
187+ }
188+
189+ @ Override
190+ public void handleStatement (Statement st ) throws RDFHandlerException {
191+ delegate .handleStatement (st );
192+ incrementCurrentSize ();
193+ }
194+
195+ @ Override
196+ public void handleComment (String comment ) throws RDFHandlerException {
197+ delegate .handleComment (comment );
198+ incrementCurrentSize ();
199+ }
200+
201+ private void incrementCurrentSize () {
202+ currentSize ++;
203+ if (currentSize > maxSize ) {
204+ endRDF ();
205+ logger .trace (
206+ "Limited size reached, throwing LimitedSizeReachedException to signal that we are done testing the export of statements for exceptions." );
207+ throw new LimitedSizeReachedException ();
118208 }
119209 }
120210 }
121211
212+ private static class LimitedSizeReachedException extends RuntimeException {
213+ @ Override
214+ public Throwable fillInStackTrace () {
215+ // Do not fill in the stack trace to avoid performance overhead
216+ return this ;
217+ }
218+ }
219+
122220}
0 commit comments