/*
* 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.sparql.engine.http;
import java.io.InputStream ;
import java.net.MalformedURLException ;
import java.net.URL ;
import java.util.Map;
import java.util.regex.Pattern ;
import org.apache.http.client.HttpClient ;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.jena.atlas.web.HttpException ;
import org.apache.jena.atlas.web.TypedInputStream ;
import org.apache.jena.query.ARQ ;
import org.apache.jena.query.QueryExecException ;
import org.apache.jena.riot.WebContent ;
import org.apache.jena.riot.web.HttpOp ;
import org.apache.jena.shared.JenaException ;
import org.apache.jena.sparql.util.Context;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
/**
* Create an execution object for performing a query on a model over HTTP. This
* is the main protocol engine for HTTP query. There are higher level classes
* for doing a query and presenting the results in an API fashion.
*
* If the query string is large, then HTTP POST is used.
*/
public class HttpQuery extends Params {
static final Logger log = LoggerFactory.getLogger(HttpQuery.class.getName());
/** The definition of "large" queries */
// Not final so that other code can change it.
static public/* final */int urlLimit = 2 * 1024;
String serviceURL;
String contentTypeResult = WebContent.contentTypeResultsXML;
// An object indicate no value associated with parameter name
final static Object noValue = new Object();
private int responseCode = 0;
private String responseMessage = null;
private boolean forcePOST = false;
private String queryString = null;
private boolean serviceParams = false;
private final Pattern queryParamPattern = Pattern.compile(".+[&|\\?]query=.*");
private int connectTimeout = 0, readTimeout = 0;
private boolean allowCompression = false;
private HttpClient client;
private HttpClientContext context;
/**
* Create a execution object for a whole model GET
*
* @param serviceURL
* The model
*/
public HttpQuery(String serviceURL) {
init(serviceURL);
}
/**
* Create a execution object for a whole model GET
*
* @param url
* The model
*/
public HttpQuery(URL url) {
init(url.toString());
}
private void init(String serviceURL) {
if (log.isTraceEnabled())
log.trace("URL: " + serviceURL);
if (serviceURL.indexOf('?') >= 0)
serviceParams = true;
if (queryParamPattern.matcher(serviceURL).matches())
throw new QueryExecException("SERVICE URL overrides the 'query' SPARQL protocol parameter");
this.serviceURL = serviceURL;
}
private String getQueryString() {
if (queryString == null)
queryString = super.httpString();
return queryString;
}
/**
* Set the content type (Accept header) for the results
*
* @param contentType
* Accept content type
*/
public void setAccept(String contentType) {
contentTypeResult = contentType;
}
/**
* Gets the Content Type
* <p>
* If the query has been made this reflects the Content-Type header returns,
* if it has not been made this reflects only the Accept header that will be
* sent (as set via the {@link #setAccept(String)} method)
* </p>
*
* @return Content Type
*/
public String getContentType() {
return contentTypeResult;
}
/**
* Gets the HTTP Response Code returned by the request (returns 0 if request
* has yet to be made)
*
* @return Response Code
*/
public int getResponseCode() {
return responseCode;
}
/**
* Gets the HTTP Response Message returned by the request (returns null if request
* has yet to be made)
*
* @return Response Message
*/
public String getResponseMessage() {
return responseMessage;
}
/**
* Sets whether the HTTP request will include compressed encoding
* header
*
* @param allow
* Whether to allow compressed encoding
*/
public void setAllowCompression(boolean allow) {
allowCompression = allow;
}
/**
* Sets the client to use
* @param client Client
*/
public void setClient(HttpClient client) {
this.client = client;
}
/**
* Sets the context to use
* @param context HTTP context
*/
public void setContext(HttpClientContext context) {
this.context = context;
}
/**
* Gets the HTTP client that is being used, may be null if no request has yet been made
* @return HTTP Client or null
*/
public HttpClient getClient() {
Context arqContext = ARQ.getContext();
if (arqContext.isDefined(Service.serviceContext)) {
@SuppressWarnings("unchecked")
Map<String, Context> context = (Map<String, Context>) arqContext.get(Service.serviceContext);
if (context.containsKey(serviceURL)) {
Context serviceContext = context.get(serviceURL);
if (serviceContext.isDefined(Service.queryClient)) return serviceContext.get(Service.queryClient);
}
}
return client;
}
/**
* Gets the HTTP context that is being used, or sets and returns a default
* @return the {@code HttpClientContext} in scope
*/
public HttpClientContext getContext() {
if (context == null) context = new HttpClientContext();
return context;
}
/**
* Return whether this request will go by GET or POST
*
* @return boolean
*/
public boolean usesPOST() {
if (forcePOST)
return true;
String s = getQueryString();
return serviceURL.length() + s.length() >= urlLimit;
}
/**
* Force the use of HTTP POST for the query operation
*/
public void setForcePOST() {
forcePOST = true;
}
/**
* Sets HTTP Connection timeout, any value <= 0 is taken to mean no timeout
*
* @param timeout
* Connection Timeout
*/
public void setConnectTimeout(int timeout) {
connectTimeout = timeout;
}
/**
* Gets the HTTP Connection timeout
*
* @return Connection Timeout
*/
public int getConnectTimeout() {
return connectTimeout;
}
/**
* Sets HTTP Read timeout, any value <= 0 is taken to mean no timeout
*
* @param timeout
* Read Timeout
*/
public void setReadTimeout(int timeout) {
readTimeout = timeout;
}
/**
* Gets the HTTP Read timeout
*
* @return Read Timeout
*/
public int getReadTimeout() {
return readTimeout;
}
/**
* Execute the operation
*
* @return Model The resulting model
* @throws QueryExceptionHTTP
*/
public InputStream exec() throws QueryExceptionHTTP {
// Select the appropriate HttpClient to use
contextualizeCompressionSettings();
contextualizeTimeoutSettings();
try {
if (usesPOST())
return execPost();
return execGet();
} catch (QueryExceptionHTTP httpEx) {
log.trace("Exception in exec", httpEx);
throw httpEx;
} catch (JenaException jEx) {
log.trace("JenaException in exec", jEx);
throw jEx;
}
}
private void contextualizeCompressionSettings() {
final RequestConfig.Builder builder = RequestConfig.copy(getContext().getRequestConfig());
builder.setContentCompressionEnabled(allowCompression);
context.setRequestConfig(builder.build());
}
private void contextualizeTimeoutSettings() {
final RequestConfig.Builder builder = RequestConfig.copy(context.getRequestConfig());
if (connectTimeout > 0) builder.setConnectTimeout(connectTimeout);
context.setRequestConfig(builder.build());
}
private InputStream execGet() throws QueryExceptionHTTP {
URL target = null;
String qs = getQueryString();
ARQ.getHttpRequestLogger().trace(qs);
try {
if (count() == 0)
target = new URL(serviceURL);
else
target = new URL(serviceURL + (serviceParams ? "&" : "?") + qs);
} catch (MalformedURLException malEx) {
throw new QueryExceptionHTTP(0, "Malformed URL: " + malEx);
}
log.trace("GET " + target.toExternalForm());
try {
try {
// Get the actual response stream
TypedInputStream stream = HttpOp.execHttpGet(target.toString(), contentTypeResult, client, getContext());
if (stream == null)
throw new QueryExceptionHTTP(404);
return execCommon(stream);
} catch (HttpException httpEx) {
// Back-off and try POST if something complain about long URIs
if (httpEx.getResponseCode() == 414)
return execPost();
throw httpEx;
}
} catch (HttpException httpEx) {
throw rewrap(httpEx);
}
}
private InputStream execPost() throws QueryExceptionHTTP {
URL target = null;
try {
target = new URL(serviceURL);
} catch (MalformedURLException malEx) {
throw new QueryExceptionHTTP(0, "Malformed URL: " + malEx);
}
log.trace("POST " + target.toExternalForm());
ARQ.getHttpRequestLogger().trace(target.toExternalForm());
try {
// Get the actual response stream
TypedInputStream stream = HttpOp.execHttpPostFormStream(serviceURL, this, contentTypeResult, client, getContext());
if (stream == null)
throw new QueryExceptionHTTP(404);
return execCommon(stream);
} catch (HttpException httpEx) {
throw rewrap(httpEx);
}
}
private QueryExceptionHTTP rewrap(HttpException httpEx) {
// The historical contract of HTTP Queries has been to throw QueryExceptionHTTP however using the standard
// ARQ HttpOp machinery we use these days means the internal HTTP errors come back as HttpException
// Therefore we need to unnwrap and re-wrap appropriately
responseCode = httpEx.getResponseCode();
if (responseCode != -1) {
// Was an actual HTTP error
String responseLine = httpEx.getStatusLine() != null ? httpEx.getStatusLine() : "No Status Line";
return new QueryExceptionHTTP(responseCode, "HTTP " + responseCode + " error making the query: " + responseLine, httpEx.getCause());
} else if (httpEx.getMessage() != null) {
// Some non-HTTP error with a valid message e.g. Socket Communications failed, IO error
return new QueryExceptionHTTP("Unexpected error making the query: " + httpEx.getMessage(), httpEx.getCause());
} else if (httpEx.getCause() != null) {
// Some other error with a cause e.g. Socket Communications failed, IO error
return new QueryExceptionHTTP("Unexpected error making the query, see cause for further details", httpEx.getCause());
} else {
// Some other error with no message and no further cause
return new QueryExceptionHTTP("Unexpected error making the query", httpEx);
}
}
private InputStream execCommon(TypedInputStream stream) throws QueryExceptionHTTP {
// Assume response code must be 200 if we got here
responseCode = 200;
responseMessage = "OK" ;
// Get the returned content type so we can expose this later via the
// getContentType() method
// We strip any parameters off the returned content type e.g.
// ;charset=UTF-8 since code that
// consumes our getContentType() method will expect a bare MIME type
contentTypeResult = stream.getContentType();
if (contentTypeResult != null && contentTypeResult.contains(";")) {
contentTypeResult = contentTypeResult.substring(0, contentTypeResult.indexOf(';'));
}
// NB - Content Encoding is now handled at a higher level
// so we don't have to worry about wrapping the stream at all
return stream;
}
@Override
public String toString() {
String s = httpString();
if (s != null && s.length() > 0)
return serviceURL + "?" + s;
return serviceURL;
}
}