You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2024/02/28 09:19:20 UTC

(jena) 01/02: GH-2233: Improved handling of specifically set HTTP headers in RDFLinkHTTP

This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit fcd6f37125b181f8b2121ec5e8f02bc028436249
Author: Claus Stadler <Ra...@googlemail.com>
AuthorDate: Sat Feb 10 02:21:02 2024 +0100

    GH-2233: Improved handling of specifically set HTTP headers in RDFLinkHTTP
---
 .../org/apache/jena/http/sys/ExecHTTPBuilder.java  |   2 +-
 .../jena/sparql/exec/http/QueryExecHTTP.java       |   5 +
 .../sparql/exec/http/QueryExecHTTPBuilder.java     |   2 +-
 .../apache/jena/test/rdflink/TestRDFLinkHTTP.java  |   8 +
 .../java/org/apache/jena/rdflink/RDFLinkHTTP.java  | 175 ++++++++++++++-------
 .../jena/rdfconnection/TestRDFConnectionHTTP.java  |  97 ++++++++++++
 6 files changed, 227 insertions(+), 62 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java b/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java
index 2aadd76542..a1f1340b17 100644
--- a/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java
@@ -109,7 +109,7 @@ public abstract class ExecHTTPBuilder<X, Y> {
     }
 
     /** Set the query */
-    private void setQuery(Query query, String queryStr) {
+    protected void setQuery(Query query, String queryStr) {
         this.query = query;
         this.queryString = queryStr;
     }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
index 5e43d7ab64..76ebeee9ab 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
@@ -147,6 +147,11 @@ public class QueryExecHTTP implements QueryExec {
         this.httpClient = HttpLib.dft(httpClient, HttpEnv.getDftHttpClient());
     }
 
+    /** Getter for the appProvidedAcceptHeader. Only used for testing. */
+    public String getAppProvidedAcceptHeader() {
+        return appProvidedAcceptHeader;
+    }
+
     /** The Content-Type response header received (null before the remote operation is attempted). */
     public String getHttpResponseContentType() {
         return httpResponseContentType;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTPBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTPBuilder.java
index 2ee78a9a03..17afa987fb 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTPBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTPBuilder.java
@@ -36,7 +36,7 @@ public class QueryExecHTTPBuilder extends ExecHTTPBuilder<QueryExecHTTP, QueryEx
 
     public static QueryExecHTTPBuilder service(String serviceURL) { return create().endpoint(serviceURL); }
 
-    private QueryExecHTTPBuilder() {}
+    protected QueryExecHTTPBuilder() {}
 
     @Override
     protected QueryExecHTTPBuilder thisBuilder() {
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/test/rdflink/TestRDFLinkHTTP.java b/jena-integration-tests/src/test/java/org/apache/jena/test/rdflink/TestRDFLinkHTTP.java
index 5f2aa59481..aaf51be4c3 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/test/rdflink/TestRDFLinkHTTP.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/test/rdflink/TestRDFLinkHTTP.java
@@ -109,4 +109,12 @@ public class TestRDFLinkHTTP extends AbstractTestRDFLink {
             }
         }
     }
+
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_3() {
+        RDFLink link = link(true);
+        try (link) {
+            link.query("custom");
+        }
+    }
 }
diff --git a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java
index 392cd16011..7bc7fed84d 100644
--- a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java
+++ b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java
@@ -37,8 +37,10 @@ import org.apache.jena.sparql.engine.binding.Binding;
 import org.apache.jena.sparql.exec.*;
 import org.apache.jena.sparql.exec.http.DSP;
 import org.apache.jena.sparql.exec.http.GSP;
+import org.apache.jena.sparql.exec.http.QueryExecHTTP;
 import org.apache.jena.sparql.exec.http.QueryExecHTTPBuilder;
 import org.apache.jena.sparql.exec.http.UpdateExecHTTPBuilder;
+import org.apache.jena.sparql.util.Context;
 import org.apache.jena.system.Txn;
 import org.apache.jena.update.UpdateFactory;
 import org.apache.jena.update.UpdateRequest;
@@ -244,74 +246,127 @@ public class RDFLinkHTTP implements RDFLink {
         return createQExecBuilder();
     }
 
-    // Create the QExec
-
+    /** Create the QExec initialized with a query object or string. */
     private QueryExec queryExec(Query query, String queryString, QueryType queryType) {
-        checkQuery();
-        if ( query == null && queryString == null )
-            throw new InternalErrorException("Both query and query string are null");
-        if ( query == null ) {
-            if ( parseCheckQueries )
-                // Don't retain the query.
-                QueryFactory.create(queryString);
+        QueryExecHTTPBuilderOverRDFLinkHTTP builder = createQExecBuilder();
+        if (queryString != null) {
+            builder.queryStringValidated(queryString, Syntax.defaultQuerySyntax, queryType);
         }
-
-        // Use the query string as provided if possible, otherwise serialize the query.
-        String queryStringToSend = ( queryString != null ) ? queryString : query.toString();
-        return createQExec(query, queryStringToSend, queryType);
+        if (query != null) {
+            builder.query(query);
+        }
+        return QueryExecApp.create(builder, null, query, queryString);
     }
 
-    // Create the QExec
+    /**
+     * An internal subclass of {@link QueryExecHTTPBuilder} that overrides
+     * {@link #buildX(HttpClient, Query, String, Context)}
+     * in order to have the accept header field derived based on this {@link RDFLinkHTTP} configuration.
+     */
+    /* package */ class QueryExecHTTPBuilderOverRDFLinkHTTP
+        extends QueryExecHTTPBuilder
+    {
+        /** The query type. Never null. */
+        protected QueryType queryType = QueryType.UNKNOWN;
 
-    /** Create a builder, configured with the link setup. */
-    private QueryExecHTTPBuilder createQExecBuilder() {
-        return QueryExecHTTPBuilder.create().endpoint(svcQuery).httpClient(httpClient);
-    }
-
-    private QueryExec createQExec(Query query, String queryStringToSend, QueryType queryType) {
-        QueryExecHTTPBuilder builder = createQExecBuilder().queryString(queryStringToSend);
-        QueryType qt = queryType;
-        if ( query != null && qt == null )
-            qt = query.queryType();
-        if ( qt == null )
-            qt = QueryType.UNKNOWN;
-        // Set the accept header - use the most specific method.
-        String requestAcceptHeader = null;
-        switch(qt) {
-            case SELECT :
-                if ( acceptSelectResult != null )
-                    requestAcceptHeader = acceptSelectResult;
-                break;
-            case ASK :
-                if ( acceptAskResult != null )
-                    requestAcceptHeader = acceptAskResult;
-                break;
-            case DESCRIBE :
-            case CONSTRUCT :
-                if ( acceptGraph != null )
-                    requestAcceptHeader = acceptGraph;
-                break;
-            case UNKNOWN:
-                // All-purpose content type.
-                if ( acceptSparqlResults != null )
-                    requestAcceptHeader = acceptSparqlResults;
-                else
-                    // No idea! Set an "anything" and hope.
-                    // (Reasonable chance this is going to end up as HTML though.)
-                    requestAcceptHeader = "*/*";
-            default :
-                break;
+        /** The parsed query is only needed for setting the appropriate HTTP header for construct quad queries. */
+        protected Query parsedQuery = null;
+
+        protected QueryExecHTTPBuilderOverRDFLinkHTTP() {
+            super();
+        }
+
+        /** This method wraps {@link #queryString(String)} and sets the additional attributes without further interpretation. */
+        protected QueryExecHTTPBuilder queryString(String queryString, QueryType queryType, Query parsedQuery) {
+            super.queryString(queryString);
+            this.queryType = queryType != null ? queryType : QueryType.UNKNOWN;
+            this.parsedQuery = parsedQuery;
+            return thisBuilder();
         }
 
-        // Make sure it was set somehow.
-        if ( requestAcceptHeader == null )
-            throw new JenaConnectionException("No Accept header");
-        if ( requestAcceptHeader != null )
-            builder.acceptHeader(requestAcceptHeader);
-        // Delayed creation so QueryExecution.setTimeout works.
+        /** {@inheritDoc} */
+        @Override
+        public QueryExecHTTPBuilder queryString(String queryString) {
+             return queryString(queryString, null, null);
+        }
+
+        /** This method is far only called from RDFLinkHTTP. It validates the query string if parseCheckQueries is enabled.  */
+        protected QueryExecHTTPBuilder queryStringValidated(String queryString, Syntax syntax, QueryType fallbackQueryType) {
+            Query parsedQuery = RDFLinkHTTP.this.parseCheckQueries
+                    ? QueryFactory.create(queryString, syntax)
+                    : null;
+
+            QueryType finalQueryType = parsedQuery != null ? parsedQuery.queryType() : fallbackQueryType;
+            return queryString(queryString, finalQueryType, parsedQuery);
+        }
 
-        builder.queryString(queryStringToSend);
-        return QueryExecApp.create(builder, null, query, queryStringToSend);
+        @Override
+        protected void setQuery(Query query, String queryStr) {
+            super.setQuery(query, queryStr);
+            parsedQuery = query;
+            queryType = query == null ? QueryType.UNKNOWN : query.queryType();
+        }
+
+        @Override
+        protected QueryExecHTTP buildX(HttpClient hClient, Query queryActual, String queryStringActual, Context cxt) {
+            // If the appAcceptHeader has not been explicitly set then derive it.
+            if (appAcceptHeader == null) {
+                String requestAcceptHeader = null;
+                // Use the most specific method.
+                switch(queryType) {
+                    case SELECT :
+                        if ( acceptSelectResult != null )
+                            requestAcceptHeader = RDFLinkHTTP.this.acceptSelectResult;
+                        break;
+                    case ASK :
+                        if ( acceptAskResult != null )
+                            requestAcceptHeader = RDFLinkHTTP.this.acceptAskResult;
+                        break;
+                    case DESCRIBE :
+                    case CONSTRUCT :
+                        // Best effort to handle construct quads. Requires a parsed query.
+                        if (parsedQuery != null && parsedQuery.isConstructQuad()) {
+                            if ( acceptDataset != null )
+                                requestAcceptHeader = RDFLinkHTTP.this.acceptDataset;
+                        } else {
+                            if ( acceptGraph != null )
+                                requestAcceptHeader = RDFLinkHTTP.this.acceptGraph;
+                        }
+                        break;
+                    case UNKNOWN:
+                        // All-purpose content type.
+                        if ( acceptSparqlResults != null ) {
+                            requestAcceptHeader = RDFLinkHTTP.this.acceptSparqlResults;
+                        } else {
+                            // No idea! Set an "anything" and hope.
+                            // (Reasonable chance this is going to end up as HTML though.)
+                            requestAcceptHeader = "*/*";
+                        }
+                        break;
+                    default :
+                        break;
+                }
+                // Make sure it was set somehow.
+                if ( requestAcceptHeader == null )
+                    throw new JenaConnectionException("No Accept header");
+
+                this.acceptHeader(requestAcceptHeader);
+            }
+
+            return super.buildX(hClient, queryActual, queryStringActual, cxt);
+        }
+    }
+
+    /**
+     * Create a builder, configured with the link setup.
+     *
+     * @param presetQueryType If non-null then the builder will force execution with this query type.
+     */
+    private QueryExecHTTPBuilderOverRDFLinkHTTP createQExecBuilder() {
+        checkQuery();
+        QueryExecHTTPBuilderOverRDFLinkHTTP builder = new QueryExecHTTPBuilderOverRDFLinkHTTP();
+        builder.endpoint(svcQuery).httpClient(httpClient);
+        return builder;
     }
 
     private void acc(StringBuilder sBuff, String acceptString) {
diff --git a/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/TestRDFConnectionHTTP.java b/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/TestRDFConnectionHTTP.java
new file mode 100644
index 0000000000..e17994f5dd
--- /dev/null
+++ b/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/TestRDFConnectionHTTP.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.jena.rdfconnection;
+
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryFactory;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.http.QueryExecHTTP;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestRDFConnectionHTTP {
+
+    @Test
+    public void test_select_01() {
+        doAssert(WebContent.contentTypeXML, "SELECT * { ?s a ?o } LIMIT 10");
+    }
+
+    @Test
+    public void test_ask_01() {
+        doAssert(WebContent.contentTypeJSON, "ASK { ?s a ?o }");
+    }
+
+    @Test
+    public void test_constructTriples_01() {
+        doAssert(WebContent.contentTypeTurtle, "CONSTRUCT WHERE { ?s a ?o } LIMIT 10");
+    }
+
+    @Test
+    public void test_constructQuads_01() {
+        doAssert(WebContent.contentTypeTriG, "CONSTRUCT WHERE { GRAPH ?g{ ?s a ?o } } LIMIT 10");
+    }
+
+    @Test
+    public void test_describe_01() {
+        doAssert(WebContent.contentTypeNTriples, "DESCRIBE <urn:x>");
+    }
+
+    private static RDFConnectionRemoteBuilder configureHeader(Query query, String header, RDFConnectionRemoteBuilder builder) {
+        return switch (query.queryType()) {
+        case SELECT -> builder.acceptHeaderSelectQuery(header);
+        case CONSTRUCT -> !query.isConstructQuad()
+                            ? builder.acceptHeaderGraph(header)
+                            : builder.acceptHeaderDataset(header);
+        case DESCRIBE -> builder.acceptHeaderGraph(header);
+        case ASK -> builder.acceptHeaderAskQuery(header);
+        default -> throw new UnsupportedOperationException("Unhandled query type for query: " + query);
+        };
+    }
+
+    /** Test whether the constructed QueryExec instance has the expected HTTP header set */
+    private static void doAssert(String expectedHeader, String queryString) {
+        Query query = QueryFactory.create(queryString);
+
+        try (RDFConnection conn = configureHeader(query, expectedHeader, RDFConnectionRemote.newBuilder())
+                .queryEndpoint("https://www.example.org/sparql") // Should never be resolved
+                .build()) {
+            try (QueryExecution qe = conn.query(queryString)) {
+                QueryExecHTTP qeh = (QueryExecHTTP)QueryExec.adapt(qe);
+                Assert.assertEquals(expectedHeader, qeh.getAppProvidedAcceptHeader());
+            }
+
+            try (QueryExecution qe = conn.newQuery().query(queryString).build()) {
+                QueryExecHTTP qeh = (QueryExecHTTP)QueryExec.adapt(qe);
+                Assert.assertEquals(expectedHeader, qeh.getAppProvidedAcceptHeader());
+            }
+
+            try (QueryExecution qe = conn.query(query)) {
+                QueryExecHTTP qeh = (QueryExecHTTP)QueryExec.adapt(qe);
+                Assert.assertEquals(expectedHeader, qeh.getAppProvidedAcceptHeader());
+            }
+
+            try (QueryExecution qe = conn.newQuery().query(query).build()) {
+                QueryExecHTTP qeh = (QueryExecHTTP)QueryExec.adapt(qe);
+                Assert.assertEquals(expectedHeader, qeh.getAppProvidedAcceptHeader());
+            }
+        }
+    }
+}