Java tutorial
/******************************************************************************* * 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. *******************************************************************************/ package org.eclipse.rdf4j.http.client; import static org.eclipse.rdf4j.http.protocol.Protocol.ACCEPT_PARAM_NAME; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import org.apache.http.Header; import org.apache.http.HeaderElement; import org.apache.http.HttpEntity; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.AuthCache; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicAuthCache; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreConnectionPNames; import org.apache.http.params.HttpParams; import org.apache.http.util.EntityUtils; import org.eclipse.rdf4j.RDF4JConfigException; import org.eclipse.rdf4j.RDF4JException; import org.eclipse.rdf4j.http.protocol.Protocol; import org.eclipse.rdf4j.http.protocol.UnauthorizedException; import org.eclipse.rdf4j.http.protocol.error.ErrorInfo; import org.eclipse.rdf4j.http.protocol.error.ErrorType; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.query.Binding; import org.eclipse.rdf4j.query.Dataset; import org.eclipse.rdf4j.query.GraphQueryResult; import org.eclipse.rdf4j.query.MalformedQueryException; import org.eclipse.rdf4j.query.QueryInterruptedException; import org.eclipse.rdf4j.query.QueryLanguage; import org.eclipse.rdf4j.query.QueryResultHandlerException; import org.eclipse.rdf4j.query.TupleQueryResult; import org.eclipse.rdf4j.query.TupleQueryResultHandler; import org.eclipse.rdf4j.query.TupleQueryResultHandlerException; import org.eclipse.rdf4j.query.UnsupportedQueryLanguageException; import org.eclipse.rdf4j.query.resultio.BooleanQueryResultFormat; import org.eclipse.rdf4j.query.resultio.BooleanQueryResultParser; import org.eclipse.rdf4j.query.resultio.BooleanQueryResultParserRegistry; import org.eclipse.rdf4j.query.resultio.QueryResultFormat; import org.eclipse.rdf4j.query.resultio.QueryResultIO; import org.eclipse.rdf4j.query.resultio.QueryResultParseException; import org.eclipse.rdf4j.query.resultio.TupleQueryResultFormat; import org.eclipse.rdf4j.query.resultio.TupleQueryResultParser; import org.eclipse.rdf4j.query.resultio.TupleQueryResultParserRegistry; import org.eclipse.rdf4j.query.resultio.helpers.QueryResultCollector; import org.eclipse.rdf4j.repository.RepositoryException; import org.eclipse.rdf4j.rio.ParserConfig; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFHandler; import org.eclipse.rdf4j.rio.RDFHandlerException; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.RDFParserRegistry; import org.eclipse.rdf4j.rio.Rio; import org.eclipse.rdf4j.rio.UnsupportedRDFormatException; import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; import org.eclipse.rdf4j.rio.helpers.ParseErrorLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; /** * The SPARQLProtocolSession provides low level HTTP methods for communication with SPARQL endpoints. All * methods are compliant to the <a href="https://www.w3.org/TR/sparql11-protocol/">SPARQL 1.1 Protocol W3C * Recommendation</a>. * <p/> * For both Tuple and Graph queries there is a variant which parses the result in the background, see * {@link BackgroundTupleResult} and {@link BackgroundGraphResult}. For boolean queries the result is parsed * in the current thread. All methods in this class guarantee that HTTP connections are closed properly and * returned to the connection pool. The methods in this class are not guaranteed to be thread-safe. * <p/> * Functionality specific to the RDF4J HTTP protocol can be found in {@link RDF4JProtocolSession} (which is * used by HTTPRepository). * * @author Herko ter Horst * @author Arjohn Kampman * @author Andreas Schwarte * @author Jeen Broekstra * @see RDF4JProtocolSession * @see <a href="https://www.w3.org/TR/sparql11-protocol/">SPARQL 1.1 Protocol (W3C Recommendation)</a> */ public class SPARQLProtocolSession implements HttpClientDependent { /*-----------* * Constants * *-----------*/ protected static final Charset UTF8 = Charset.forName("UTF-8"); /** * The default value of the threshold for URL length, beyond which we use the POST method for SPARQL query * requests. The default is based on the lowest common denominator for various web servers. */ public static final int DEFAULT_MAXIMUM_URL_LENGTH = 4083; /** * @deprecated use {@link #DEFAULT_MAXIMUM_URL_LENGTH} instead. */ @Deprecated public static final int MAXIMUM_URL_LENGTH = DEFAULT_MAXIMUM_URL_LENGTH; /** * System property for configuration of URL length threshold: {@code rdf4j.sparql.url.maxlength}. A * threshold of 0 (or a negative value) means that the POST method is used for <strong>every</strong> * SPARQL query request. */ public static final String MAXIMUM_URL_LENGTH_PARAM = "rdf4j.sparql.url.maxlength"; /** * The threshold for URL length, beyond which we use the POST method. A threshold of 0 (or a negative * value) means that the POST method is used for <strong>every</strong> SPARQL query request. */ private final int maximumUrlLength; final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * shared instance of a {@link Joiner} for creating a comma-separated string. */ private static final Joiner commaJoiner = Joiner.on(", "); /*-----------* * Variables * *-----------*/ private ValueFactory valueFactory; private String queryURL; private String updateURL; private HttpClient httpClient; private final ExecutorService executor; private final HttpClientContext httpContext; private HttpParams params; private ParserConfig parserConfig = new ParserConfig(); private TupleQueryResultFormat preferredTQRFormat = TupleQueryResultFormat.BINARY; private BooleanQueryResultFormat preferredBQRFormat = BooleanQueryResultFormat.TEXT; private RDFFormat preferredRDFFormat = RDFFormat.TURTLE; private Map<String, String> additionalHttpHeaders = Collections.emptyMap(); /*--------------* * Constructors * *--------------*/ public SPARQLProtocolSession(HttpClient client, ExecutorService executor) { this.httpClient = client; this.httpContext = new HttpClientContext(); this.executor = executor; valueFactory = SimpleValueFactory.getInstance(); httpContext.setCookieStore(new BasicCookieStore()); // parser used for processing server response data should be lenient parserConfig.addNonFatalError(BasicParserSettings.VERIFY_DATATYPE_VALUES); parserConfig.addNonFatalError(BasicParserSettings.VERIFY_LANGUAGE_TAGS); // configure the maximum url length for SPARQL query GET requests int maximumUrlLength = DEFAULT_MAXIMUM_URL_LENGTH; String propertyValue = System.getProperty(MAXIMUM_URL_LENGTH_PARAM); if (propertyValue != null) { try { maximumUrlLength = Integer.parseInt(propertyValue); } catch (NumberFormatException e) { throw new RDF4JConfigException("integer value expected for property " + MAXIMUM_URL_LENGTH_PARAM, e); } } this.maximumUrlLength = maximumUrlLength; } /*-----------------* * Get/set methods * *-----------------*/ public final HttpClient getHttpClient() { return httpClient; } public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } public void setValueFactory(ValueFactory valueFactory) { this.valueFactory = valueFactory; } public ValueFactory getValueFactory() { return valueFactory; } protected void setQueryURL(String queryURL) { if (queryURL == null) { throw new IllegalArgumentException("queryURL must not be null"); } this.queryURL = queryURL; } protected void setUpdateURL(String updateURL) { if (updateURL == null) { throw new IllegalArgumentException("updateURL must not be null"); } this.updateURL = updateURL; } /** * Sets the preferred format for encoding tuple query results. The {@link TupleQueryResultFormat#BINARY * binary} format is preferred by default. * * @param format * The preferred {@link TupleQueryResultFormat}, or <tt>null</tt> to indicate no specific format is * preferred. */ public void setPreferredTupleQueryResultFormat(TupleQueryResultFormat format) { preferredTQRFormat = format; } /** * Gets the preferred {@link TupleQueryResultFormat} for encoding tuple query results. * * @return The preferred format, of <tt>null</tt> if no specific format is preferred. */ public TupleQueryResultFormat getPreferredTupleQueryResultFormat() { return preferredTQRFormat; } /** * Sets the preferred format for encoding RDF documents. The {@link RDFFormat#TURTLE Turtle} format is * preferred by default. * * @param format * The preferred {@link RDFFormat}, or <tt>null</tt> to indicate no specific format is preferred. */ public void setPreferredRDFFormat(RDFFormat format) { preferredRDFFormat = format; } /** * Gets the preferred {@link RDFFormat} for encoding RDF documents. * * @return The preferred format, of <tt>null</tt> if no specific format is preferred. */ public RDFFormat getPreferredRDFFormat() { return preferredRDFFormat; } /** * Sets the preferred format for encoding boolean query results. The {@link BooleanQueryResultFormat#TEXT * binary} format is preferred by default. * * @param format * The preferred {@link BooleanQueryResultFormat}, or <tt>null</tt> to indicate no specific format * is preferred. */ public void setPreferredBooleanQueryResultFormat(BooleanQueryResultFormat format) { preferredBQRFormat = format; } /** * Gets the preferred {@link BooleanQueryResultFormat} for encoding boolean query results. * * @return The preferred format, of <tt>null</tt> if no specific format is preferred. */ public BooleanQueryResultFormat getPreferredBooleanQueryResultFormat() { return preferredBQRFormat; } /** * Set the username and password for authentication with the remote server. * * @param username * the username * @param password * the password */ public void setUsernameAndPassword(String username, String password) { setUsernameAndPasswordForUrl(username, password, getQueryURL()); } protected void setUsernameAndPasswordForUrl(String username, String password, String url) { if (username != null && password != null) { logger.debug("Setting username '{}' and password for server at {}.", username, url); java.net.URI requestURI = java.net.URI.create(url); String host = requestURI.getHost(); int port = requestURI.getPort(); AuthScope scope = new AuthScope(host, port); UsernamePasswordCredentials cred = new UsernamePasswordCredentials(username, password); CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(scope, cred); httpContext.setCredentialsProvider(credsProvider); AuthCache authCache = new BasicAuthCache(); BasicScheme basicAuth = new BasicScheme(); HttpHost httpHost = new HttpHost(requestURI.getHost(), requestURI.getPort(), requestURI.getScheme()); authCache.put(httpHost, basicAuth); httpContext.setAuthCache(authCache); } else { httpContext.removeAttribute(HttpClientContext.AUTH_CACHE); httpContext.removeAttribute(HttpClientContext.CREDS_PROVIDER); } } protected void execute(Runnable command) { executor.execute(command); } public String getQueryURL() { return queryURL; } public String getUpdateURL() { return updateURL; } /*------------------* * Query evaluation * *------------------*/ public TupleQueryResult sendTupleQuery(QueryLanguage ql, String query, Dataset dataset, boolean includeInferred, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { return sendTupleQuery(ql, query, null, dataset, includeInferred, 0, bindings); } public TupleQueryResult sendTupleQuery(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { HttpUriRequest method = getQueryMethod(ql, query, baseURI, dataset, includeInferred, maxQueryTime, bindings); return getBackgroundTupleQueryResult(method); } public void sendTupleQuery(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, TupleQueryResultHandler handler, Binding... bindings) throws IOException, TupleQueryResultHandlerException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { HttpUriRequest method = getQueryMethod(ql, query, baseURI, dataset, includeInferred, maxQueryTime, bindings); getTupleQueryResult(method, handler); } public void sendUpdate(QueryLanguage ql, String update, String baseURI, Dataset dataset, boolean includeInferred, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { sendUpdate(ql, update, baseURI, dataset, includeInferred, 0, bindings); } public void sendUpdate(QueryLanguage ql, String update, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { HttpUriRequest method = getUpdateMethod(ql, update, baseURI, dataset, includeInferred, maxQueryTime, bindings); try { executeNoContent(method); } catch (RepositoryException e) { throw e; } catch (MalformedQueryException e) { throw e; } catch (QueryInterruptedException e) { throw e; } catch (RDF4JException e) { throw new RepositoryException(e); } } public GraphQueryResult sendGraphQuery(QueryLanguage ql, String query, Dataset dataset, boolean includeInferred, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { return sendGraphQuery(ql, query, null, dataset, includeInferred, 0, bindings); } public GraphQueryResult sendGraphQuery(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { try { HttpUriRequest method = getQueryMethod(ql, query, baseURI, dataset, includeInferred, maxQueryTime, bindings); return getRDFBackground(method, false); } catch (RDFHandlerException e) { // Found a bug in TupleQueryResultBuilder? throw new RepositoryException(e); } } public void sendGraphQuery(QueryLanguage ql, String query, Dataset dataset, boolean includeInferred, RDFHandler handler, Binding... bindings) throws IOException, RDFHandlerException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { sendGraphQuery(ql, query, null, dataset, includeInferred, 0, handler, bindings); } public void sendGraphQuery(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, RDFHandler handler, Binding... bindings) throws IOException, RDFHandlerException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { HttpUriRequest method = getQueryMethod(ql, query, baseURI, dataset, includeInferred, maxQueryTime, bindings); getRDF(method, handler, false); } public boolean sendBooleanQuery(QueryLanguage ql, String query, Dataset dataset, boolean includeInferred, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { return sendBooleanQuery(ql, query, null, dataset, includeInferred, 0, bindings); } public boolean sendBooleanQuery(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) throws IOException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { HttpUriRequest method = getQueryMethod(ql, query, baseURI, dataset, includeInferred, maxQueryTime, bindings); try { return getBoolean(method); } catch (RepositoryException e) { throw e; } catch (MalformedQueryException e) { throw e; } catch (QueryInterruptedException e) { throw e; } catch (RDF4JException e) { throw new RepositoryException(e); } } /** * Get the additional HTTP headers which will be used * * @return a read-only view of the additional HTTP headers which will be included in every request to the * server. */ public Map<String, String> getAdditionalHttpHeaders() { return Collections.unmodifiableMap(additionalHttpHeaders); } /** * Set additional HTTP headers to be included in every request to the server, which may be required for * certain unusual server configurations. * * @param additionalHttpHeaders * a map containing pairs of header names and values. May be null */ public void setAdditionalHttpHeaders(Map<String, String> additionalHttpHeaders) { if (additionalHttpHeaders == null) { this.additionalHttpHeaders = Collections.emptyMap(); } else { this.additionalHttpHeaders = additionalHttpHeaders; } } protected HttpUriRequest getQueryMethod(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) { List<NameValuePair> queryParams = getQueryMethodParameters(ql, query, baseURI, dataset, includeInferred, maxQueryTime, bindings); HttpUriRequest method; String queryUrlWithParams; try { URIBuilder urib = new URIBuilder(getQueryURL()); for (NameValuePair nvp : queryParams) urib.addParameter(nvp.getName(), nvp.getValue()); queryUrlWithParams = urib.toString(); } catch (URISyntaxException e) { throw new AssertionError(e); } if (shouldUsePost(queryUrlWithParams)) { // we just built up a URL for nothing. oh well. // It's probably not much overhead against // the poor triplestore having to process such as massive query HttpPost postMethod = new HttpPost(getQueryURL()); postMethod.setHeader("Content-Type", Protocol.FORM_MIME_TYPE + "; charset=utf-8"); postMethod.setEntity(new UrlEncodedFormEntity(queryParams, UTF8)); method = postMethod; } else { method = new HttpGet(queryUrlWithParams); } // functionality to provide custom http headers as required by the // applications for (Map.Entry<String, String> additionalHeader : additionalHttpHeaders.entrySet()) { method.addHeader(additionalHeader.getKey(), additionalHeader.getValue()); } return method; } /** * Return whether the provided query should use POST (otherwise use GET) * * @param fullQueryUrl * the complete URL, including hostname and all HTTP query parameters */ protected boolean shouldUsePost(String fullQueryUrl) { return fullQueryUrl.length() > maximumUrlLength; } protected HttpUriRequest getUpdateMethod(QueryLanguage ql, String update, String baseURI, Dataset dataset, boolean includeInferred, Binding... bindings) { return getUpdateMethod(ql, update, baseURI, dataset, includeInferred, 0, bindings); } protected HttpUriRequest getUpdateMethod(QueryLanguage ql, String update, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) { HttpPost method = new HttpPost(getUpdateURL()); method.setHeader("Content-Type", Protocol.FORM_MIME_TYPE + "; charset=utf-8"); List<NameValuePair> queryParams = getUpdateMethodParameters(ql, update, baseURI, dataset, includeInferred, maxQueryTime, bindings); method.setEntity(new UrlEncodedFormEntity(queryParams, UTF8)); if (this.additionalHttpHeaders != null) { for (Map.Entry<String, String> additionalHeader : additionalHttpHeaders.entrySet()) method.addHeader(additionalHeader.getKey(), additionalHeader.getValue()); } return method; } protected List<NameValuePair> getQueryMethodParameters(QueryLanguage ql, String query, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) { List<NameValuePair> queryParams = new ArrayList<NameValuePair>(); /* * Only query, default-graph-uri, and named-graph-uri are standard parameters in SPARQL Protocol 1.1. */ if (query != null) { if (baseURI != null && !baseURI.equals("")) { // prepend query string with base URI declaration query = "BASE <" + baseURI + "> \n" + query; } queryParams.add(new BasicNameValuePair(Protocol.QUERY_PARAM_NAME, query)); } if (dataset != null) { for (IRI defaultGraphURI : dataset.getDefaultGraphs()) { queryParams.add( new BasicNameValuePair(Protocol.DEFAULT_GRAPH_PARAM_NAME, String.valueOf(defaultGraphURI))); } for (IRI namedGraphURI : dataset.getNamedGraphs()) { queryParams.add( new BasicNameValuePair(Protocol.NAMED_GRAPH_PARAM_NAME, String.valueOf(namedGraphURI))); } } return queryParams; } protected List<NameValuePair> getUpdateMethodParameters(QueryLanguage ql, String update, String baseURI, Dataset dataset, boolean includeInferred, Binding... bindings) { return getUpdateMethodParameters(ql, update, baseURI, dataset, includeInferred, 0, bindings); } protected List<NameValuePair> getUpdateMethodParameters(QueryLanguage ql, String update, String baseURI, Dataset dataset, boolean includeInferred, int maxQueryTime, Binding... bindings) { List<NameValuePair> queryParams = new ArrayList<NameValuePair>(); if (update != null) { if (baseURI != null && !baseURI.equals("")) { // prepend update string with base URI declaration update = "BASE <" + baseURI + "> \n" + update; } queryParams.add(new BasicNameValuePair(Protocol.UPDATE_PARAM_NAME, update)); logger.debug("added update string {}", update); } if (dataset != null) { if (dataset.getDefaultRemoveGraphs().size() > 0) { if (!(dataset.getDefaultRemoveGraphs().equals(dataset.getDefaultGraphs()))) { logger.warn( "ambiguous dataset spec for SPARQL endpoint: default graphs and default remove graphs both defined but not equal"); } for (IRI graphURI : dataset.getDefaultRemoveGraphs()) { if (dataset.getDefaultInsertGraph() != null) { if (!dataset.getDefaultInsertGraph().equals(graphURI)) { logger.warn( "ambiguous dataset spec for SPARQL endpoint: default insert graph ({}) and default remove graph ({}) both defined but not equal. ", dataset.getDefaultInsertGraph(), graphURI); } } queryParams .add(new BasicNameValuePair(Protocol.USING_GRAPH_PARAM_NAME, String.valueOf(graphURI))); } } if (dataset.getDefaultInsertGraph() != null) { if (!dataset.getDefaultGraphs().isEmpty()) { if (!(dataset.getDefaultGraphs().size() == 1 && dataset.getDefaultGraphs().contains(dataset.getDefaultInsertGraph()))) { logger.warn( "ambiguous dataset spec for SPARQL endpoint: default insert graph ({}) and default graphs both defined but not equal. ", dataset.getDefaultInsertGraph()); } } queryParams.add(new BasicNameValuePair(Protocol.USING_GRAPH_PARAM_NAME, String.valueOf(dataset.getDefaultInsertGraph()))); } for (IRI defaultGraphURI : dataset.getDefaultGraphs()) { queryParams.add( new BasicNameValuePair(Protocol.USING_GRAPH_PARAM_NAME, String.valueOf(defaultGraphURI))); } for (IRI namedGraphURI : dataset.getNamedGraphs()) { queryParams.add(new BasicNameValuePair(Protocol.USING_NAMED_GRAPH_PARAM_NAME, String.valueOf(namedGraphURI))); } } return queryParams; } /*------------------* * Response parsing * *------------------*/ /** * Parse the response in a background thread. HTTP connections are dealt with in the * {@link BackgroundTupleResult} or (in the error-case) in this method. */ protected BackgroundTupleResult getBackgroundTupleQueryResult(HttpUriRequest method) throws RepositoryException, QueryInterruptedException, MalformedQueryException, IOException { boolean submitted = false; // Specify which formats we support Set<QueryResultFormat> tqrFormats = TupleQueryResultParserRegistry.getInstance().getKeys(); if (tqrFormats.isEmpty()) { throw new RepositoryException("No tuple query result parsers have been registered"); } BackgroundTupleResult tRes = null; // send the tuple query HttpResponse response = sendTupleQueryViaHttp(method, tqrFormats); try { // if we get here, HTTP code is 200 String mimeType = getResponseMIMEType(response); QueryResultFormat format = TupleQueryResultFormat.matchMIMEType(mimeType, tqrFormats).orElseThrow( () -> new RepositoryException("Server responded with an unsupported file format: " + mimeType)); TupleQueryResultParser parser = QueryResultIO.createTupleParser(format, getValueFactory()); tRes = new BackgroundTupleResult(parser, response.getEntity().getContent()); execute(tRes); submitted = true; return tRes; } finally { if (!submitted) { try { if (tRes != null) { tRes.close(); } } finally { EntityUtils.consumeQuietly(response.getEntity()); } } } } /** * Parse the response in this thread using the provided {@link TupleQueryResultHandler}. All HTTP * connections are closed and released in this method */ protected void getTupleQueryResult(HttpUriRequest method, TupleQueryResultHandler handler) throws IOException, TupleQueryResultHandlerException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { // Specify which formats we support Set<QueryResultFormat> tqrFormats = TupleQueryResultParserRegistry.getInstance().getKeys(); if (tqrFormats.isEmpty()) { throw new RepositoryException("No tuple query result parsers have been registered"); } // send the tuple query HttpResponse response = sendTupleQueryViaHttp(method, tqrFormats); try { // if we get here, HTTP code is 200 String mimeType = getResponseMIMEType(response); try { QueryResultFormat format = TupleQueryResultFormat.matchMIMEType(mimeType, tqrFormats) .orElseThrow(() -> new RepositoryException( "Server responded with an unsupported file format: " + mimeType)); TupleQueryResultParser parser = QueryResultIO.createTupleParser(format, getValueFactory()); parser.setQueryResultHandler(handler); parser.parseQueryResult(response.getEntity().getContent()); } catch (QueryResultParseException e) { throw new RepositoryException("Malformed query result from server", e); } catch (QueryResultHandlerException e) { if (e instanceof TupleQueryResultHandlerException) { throw (TupleQueryResultHandlerException) e; } else { throw new TupleQueryResultHandlerException(e); } } } finally { EntityUtils.consumeQuietly(response.getEntity()); } } /** * Send the tuple query via HTTP and throws an exception in case anything goes wrong, i.e. only for HTTP * 200 the method returns without exception. If HTTP status code is not equal to 200, the request is * aborted, however pooled connections are not released. * * @param method * @throws RepositoryException * @throws HttpException * @throws IOException * @throws QueryInterruptedException * @throws MalformedQueryException */ private HttpResponse sendTupleQueryViaHttp(HttpUriRequest method, Set<QueryResultFormat> tqrFormats) throws RepositoryException, IOException, QueryInterruptedException, MalformedQueryException { final List<String> acceptValues = new ArrayList<String>(tqrFormats.size()); for (QueryResultFormat format : tqrFormats) { // Determine a q-value that reflects the user specified preference int qValue = 10; if (preferredTQRFormat != null && !preferredTQRFormat.equals(format)) { // Prefer specified format over other formats qValue -= 2; } for (String mimeType : format.getMIMETypes()) { String acceptParam = mimeType; if (qValue < 10) { acceptParam += ";q=0." + qValue; } acceptValues.add(acceptParam); } } method.addHeader(ACCEPT_PARAM_NAME, commaJoiner.join(acceptValues)); try { return executeOK(method); } catch (RepositoryException | MalformedQueryException | QueryInterruptedException e) { throw e; } catch (RDF4JException e) { throw new RepositoryException(e); } } /** * Parse the response in a background thread. HTTP connections are dealt with in the * {@link BackgroundGraphResult} or (in the error-case) in this method. */ protected BackgroundGraphResult getRDFBackground(HttpUriRequest method, boolean requireContext) throws IOException, RDFHandlerException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { boolean submitted = false; // Specify which formats we support using Accept headers Set<RDFFormat> rdfFormats = RDFParserRegistry.getInstance().getKeys(); if (rdfFormats.isEmpty()) { throw new RepositoryException("No tuple RDF parsers have been registered"); } BackgroundGraphResult gRes = null; // send the tuple query HttpResponse response = sendGraphQueryViaHttp(method, requireContext, rdfFormats); try { // if we get here, HTTP code is 200 String mimeType = getResponseMIMEType(response); RDFFormat format = RDFFormat.matchMIMEType(mimeType, rdfFormats).orElseThrow( () -> new RepositoryException("Server responded with an unsupported file format: " + mimeType)); RDFParser parser = Rio.createParser(format, getValueFactory()); parser.setParserConfig(getParserConfig()); parser.setParseErrorListener(new ParseErrorLogger()); Charset charset = null; // SES-1793 : Do not attempt to check for a charset if the format is // defined not to have a charset // This prevents errors caused by people erroneously attaching a // charset to a binary formatted document HttpEntity entity = response.getEntity(); if (format.hasCharset() && entity != null && entity.getContentType() != null) { // TODO copied from SPARQLGraphQuery repository, is this // required? try { charset = ContentType.parse(entity.getContentType().getValue()).getCharset(); } catch (IllegalCharsetNameException e) { // work around for Joseki-3.2 // Content-Type: application/rdf+xml; // charset=application/rdf+xml } if (charset == null) { charset = UTF8; } } if (entity == null) { throw new RepositoryException("Server response was empty."); } String baseURI = method.getURI().toASCIIString(); gRes = new BackgroundGraphResult(parser, entity.getContent(), charset, baseURI); execute(gRes); submitted = true; return gRes; } finally { if (!submitted) { try { if (gRes != null) { gRes.close(); } } finally { EntityUtils.consumeQuietly(response.getEntity()); } } } } /** * Parse the response in this thread using the provided {@link RDFHandler}. All HTTP connections are * closed and released in this method */ protected void getRDF(HttpUriRequest method, RDFHandler handler, boolean requireContext) throws IOException, RDFHandlerException, RepositoryException, MalformedQueryException, UnauthorizedException, QueryInterruptedException { // Specify which formats we support using Accept headers Set<RDFFormat> rdfFormats = RDFParserRegistry.getInstance().getKeys(); if (rdfFormats.isEmpty()) { throw new RepositoryException("No tuple RDF parsers have been registered"); } // send the tuple query HttpResponse response = sendGraphQueryViaHttp(method, requireContext, rdfFormats); try { String mimeType = getResponseMIMEType(response); try { RDFFormat format = RDFFormat.matchMIMEType(mimeType, rdfFormats) .orElseThrow(() -> new RepositoryException( "Server responded with an unsupported file format: " + mimeType)); RDFParser parser = Rio.createParser(format, getValueFactory()); parser.setParserConfig(getParserConfig()); parser.setParseErrorListener(new ParseErrorLogger()); parser.setRDFHandler(handler); parser.parse(response.getEntity().getContent(), method.getURI().toASCIIString()); } catch (RDFParseException e) { throw new RepositoryException("Malformed query result from server", e); } } finally { EntityUtils.consumeQuietly(response.getEntity()); } } private HttpResponse sendGraphQueryViaHttp(HttpUriRequest method, boolean requireContext, Set<RDFFormat> rdfFormats) throws RepositoryException, IOException, QueryInterruptedException, MalformedQueryException { List<String> acceptParams = RDFFormat.getAcceptParams(rdfFormats, requireContext, getPreferredRDFFormat()); method.addHeader(ACCEPT_PARAM_NAME, commaJoiner.join(acceptParams)); try { return executeOK(method); } catch (RepositoryException | MalformedQueryException | QueryInterruptedException e) { throw e; } catch (RDF4JException e) { throw new RepositoryException(e); } } /** * Parse the response in this thread using a suitable {@link BooleanQueryResultParser}. All HTTP * connections are closed and released in this method * * @throws RDF4JException */ protected boolean getBoolean(HttpUriRequest method) throws IOException, RDF4JException { // Specify which formats we support using Accept headers Set<QueryResultFormat> booleanFormats = BooleanQueryResultParserRegistry.getInstance().getKeys(); if (booleanFormats.isEmpty()) { throw new RepositoryException("No boolean query result parsers have been registered"); } // send the tuple query HttpResponse response = sendBooleanQueryViaHttp(method, booleanFormats); try { // if we get here, HTTP code is 200 String mimeType = getResponseMIMEType(response); try { QueryResultFormat format = BooleanQueryResultFormat.matchMIMEType(mimeType, booleanFormats) .orElseThrow(() -> new RepositoryException( "Server responded with an unsupported file format: " + mimeType)); BooleanQueryResultParser parser = QueryResultIO.createBooleanParser(format); QueryResultCollector results = new QueryResultCollector(); parser.setQueryResultHandler(results); parser.parseQueryResult(response.getEntity().getContent()); return results.getBoolean(); } catch (QueryResultParseException e) { throw new RepositoryException("Malformed query result from server", e); } } finally { EntityUtils.consumeQuietly(response.getEntity()); } } private HttpResponse sendBooleanQueryViaHttp(HttpUriRequest method, Set<QueryResultFormat> booleanFormats) throws IOException, RDF4JException { final List<String> acceptValues = new ArrayList<>(booleanFormats.size()); for (QueryResultFormat format : booleanFormats) { // Determine a q-value that reflects the user specified preference int qValue = 10; if (preferredBQRFormat != null && !preferredBQRFormat.equals(format)) { // Prefer specified format over other formats qValue -= 2; } for (String mimeType : format.getMIMETypes()) { String acceptParam = mimeType; if (qValue < 10) { acceptParam += ";q=0." + qValue; } acceptValues.add(acceptParam); } } method.addHeader(ACCEPT_PARAM_NAME, commaJoiner.join(acceptValues)); return executeOK(method); } /** * Convenience method to deal with HTTP level errors of tuple, graph and boolean queries in the same way. * This method aborts the HTTP connection. * * @param method * @throws RDF4JException */ protected HttpResponse executeOK(HttpUriRequest method) throws IOException, RDF4JException { boolean fail = true; HttpResponse response = execute(method); try { int httpCode = response.getStatusLine().getStatusCode(); if (httpCode == HttpURLConnection.HTTP_OK || httpCode == HttpURLConnection.HTTP_NOT_AUTHORITATIVE) { fail = false; return response; // everything OK, control flow can continue } else { // trying to contact a non-SPARQL server? throw new RepositoryException("Failed to get server protocol; no such resource on this server: " + method.getURI().toString()); } } finally { if (fail) { EntityUtils.consumeQuietly(response.getEntity()); } } } protected void executeNoContent(HttpUriRequest method) throws IOException, RDF4JException { HttpResponse response = execute(method); try { if (response.getStatusLine().getStatusCode() >= 300) { throw new RepositoryException("Failed to get server protocol; no such resource on this server: " + method.getURI().toString()); } } finally { EntityUtils.consume(response.getEntity()); } } protected HttpResponse execute(HttpUriRequest method) throws IOException, RDF4JException { boolean consume = true; if (params != null) { method.setParams(params); } HttpResponse response = httpClient.execute(method, httpContext); try { int httpCode = response.getStatusLine().getStatusCode(); if (httpCode >= 200 && httpCode < 300 || httpCode == HttpURLConnection.HTTP_NOT_FOUND) { consume = false; return response; // everything OK, control flow can continue } else { switch (httpCode) { case HttpURLConnection.HTTP_UNAUTHORIZED: // 401 throw new UnauthorizedException(); case HttpURLConnection.HTTP_UNAVAILABLE: // 503 throw new QueryInterruptedException(); default: ErrorInfo errInfo = getErrorInfo(response); // Throw appropriate exception if (errInfo.getErrorType() == ErrorType.MALFORMED_DATA) { throw new RDFParseException(errInfo.getErrorMessage()); } else if (errInfo.getErrorType() == ErrorType.UNSUPPORTED_FILE_FORMAT) { throw new UnsupportedRDFormatException(errInfo.getErrorMessage()); } else if (errInfo.getErrorType() == ErrorType.MALFORMED_QUERY) { throw new MalformedQueryException(errInfo.getErrorMessage()); } else if (errInfo.getErrorType() == ErrorType.UNSUPPORTED_QUERY_LANGUAGE) { throw new UnsupportedQueryLanguageException(errInfo.getErrorMessage()); } else if (errInfo.toString().length() > 0) { throw new RepositoryException(errInfo.toString()); } else { throw new RepositoryException(response.getStatusLine().getReasonPhrase()); } } } } finally { if (consume) { EntityUtils.consumeQuietly(response.getEntity()); } } } /*-------------------------* * General utility methods * *-------------------------*/ /** * Gets the MIME type specified in the response headers of the supplied method, if any. For example, if * the response headers contain <tt>Content-Type: application/xml;charset=UTF-8</tt>, this method will * return <tt>application/xml</tt> as the MIME type. * * @param method * The method to get the reponse MIME type from. * @return The response MIME type, or <tt>null</tt> if not available. */ protected String getResponseMIMEType(HttpResponse method) throws IOException { Header[] headers = method.getHeaders("Content-Type"); for (Header header : headers) { HeaderElement[] headerElements = header.getElements(); for (HeaderElement headerEl : headerElements) { String mimeType = headerEl.getName(); if (mimeType != null) { logger.debug("reponse MIME type is {}", mimeType); return mimeType; } } } return null; } protected ErrorInfo getErrorInfo(HttpResponse response) throws RepositoryException { try { ErrorInfo errInfo = ErrorInfo.parse(EntityUtils.toString(response.getEntity())); logger.warn("Server reports problem: {}", errInfo.getErrorMessage()); return errInfo; } catch (IOException e) { logger.warn("Unable to retrieve error info from server"); throw new RepositoryException("Unable to retrieve error info from server", e); } } /** * Sets the parser configuration used to process HTTP response data. * * @param parserConfig * The parserConfig to set. */ public void setParserConfig(ParserConfig parserConfig) { this.parserConfig = parserConfig; } /** * @return Returns the parser configuration used to process HTTP response data. */ public ParserConfig getParserConfig() { return parserConfig; } /** * Gets the http connection read timeout in milliseconds. */ public long getConnectionTimeout() { if (params == null) { return 0; } return (long) params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0); } /** * Sets the http connection read timeout. * * @param timeout * timeout in milliseconds. Zero sets to infinity. */ public void setConnectionTimeout(long timeout) { if (params == null) { params = new BasicHttpParams(); } params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, (int) timeout); } }