/**
* 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.jdbc.remote;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClients;
import org.apache.jena.jdbc.JdbcCompatibility;
import org.apache.jena.jdbc.JenaDriver;
import org.apache.jena.jdbc.connections.JenaConnection;
import org.apache.jena.jdbc.remote.connections.RemoteEndpointConnection;
import org.apache.jena.system.JenaSystem ;
/**
* <p>
* A Jena JDBC driver which creates connections to remote endpoints
* </p>
* <h3>
* Connection URL</h3>
* <p>
* This driver expects a URL of the following form:
* </p>
*
* <pre>
* jdbc:jena:remote:query=http://example.org/query&update=http://example.org/update
* </pre>
* <p>
* The {@code query} parameter is used to refer to a SPARQL query endpoint to
* use, the {@code update} parameter is used to refer to a SPARQL update
* endpoint to use. At least one of these parameters must be present in order to
* make a connection, you may omit the former to get a write only connection or
* the latter to get a read only connection.
* </p>
* <p>
* Note that since the {@code &} character is used as a separator for connection
* URL parameters if you need your endpoint URLs to include these you should set
* the relevant parameters directly on the {@link Properties} object you pass to
* the {@link #connect(String, Properties)} method instead.
* </p>
* <h3>Other Supported Parameters</h3>
* <p>
* This driver supports a variety of properties that can be used to configure
* aspects of its behavior, firstly there are a set of properties used to
* specify the dataset for queries and updates:
* </p>
* <ul>
* <li>{@code default-graph-uri} - Sets a default graph for queries, may be
* specified multiple times to specify multiple graphs to form the default graph
* </li>
* <li>{@code named-graph-uri} - Sets a named graph for queries, may be
* specified multiple times to specify multiple named graphs for the dataset</li>
* <li>{@code using-graph-uri} - Sets a default graph for updates, may be
* specified multiple times to specify multiple graphs to form the default graph
* for updates</li>
* <li>{@code using-named-graph-uri} - Sets a named graph for updates, may be
* specified multiple times to specify multiple named graphs for the dataset</li>
* <li>{@code select-results-type} - Sets the format to request {@code SELECT}
* results in from the remote endpoint</li>
* <li>{@code model-results-type} - Sets the format to request {@code CONSTRUCT}
* and {@code DESCRIBE} results in from the remote endpoint</li>
* </ul>
* <h3>Authentication Parameters</h3>
* <p>
* The driver also supports the standard JDBC {@code user} and {@code password}
* parameters which are used to set user credentials for authenticating to the
* remote HTTP server.
* </p>
* <p>
* Alternatively you may use the {@code client} parameter to set a
* specific client implementation to use, must be passed an instance of
* {@link HttpClient} so can only be passed via the {@link Properties}
* object and not via the connection URL. If this parameter is used then all
* other authentication parameters are ignored.
* </p>
*/
public class RemoteEndpointDriver extends JenaDriver {
/**
* Constant for the remote driver prefix, this is appended to the base
* {@link JenaDriver#DRIVER_PREFIX} to form the URL prefix for JDBC
* Connection URLs for this driver
*/
public static final String REMOTE_DRIVER_PREFIX = "remote:";
/**
* Constant for the connection URL parameter that sets the remote SPARQL
* query endpoint to use
*/
public static final String PARAM_QUERY_ENDPOINT = "query";
/**
* Constant for the connection URL parameter that sets the remote SPARQL
* update endpoint to use
*/
public static final String PARAM_UPDATE_ENDPOINT = "update";
/**
* Constant for the connection URL parameter that sets a default graph URI
* for SPARQL queries, may be specified multiple times to use specify
* multiple default graphs to use
*/
public static final String PARAM_DEFAULT_GRAPH_URI = "default-graph-uri";
/**
* Constant for the connection URL parameter that sets a named graph URI for
* SPARQL queries, may be specified multiple times to use specify multiple
* named graphs to use
*/
public static final String PARAM_NAMED_GRAPH_URI = "named-graph-uri";
/**
* Constant for the connection URL parameter that sets a default graph URI
* for SPARQL updates, may be specified multiple times to use specify
* multiple default graphs to use
*/
public static final String PARAM_USING_GRAPH_URI = "using-graph-uri";
/**
* Constant for the connection URL parameter that sets a named graph URI for
* SPARQL updates, may be specified multiple times to use specify multiple
* named graphs to use
*/
public static final String PARAM_USING_NAMED_GRAPH_URI = "using-named-graph-uri";
/**
* Constant for the connection URL parameter that sets the results type to
* request for {@code SELECT} queries against the remote endpoint
*/
public static final String PARAM_SELECT_RESULTS_TYPE = "select-results-type";
/**
* Constant for the connection URL parameter that sets the results type to
* request for {@code CONSTRUCT} and {@code DESCRIBE} queries against the
* remote endpoint
*/
public static final String PARAM_MODEL_RESULTS_TYPE = "model-results-type";
/**
* Constant for the parameter used to specify a client used.
* <p>
* It is <strong>important</strong> to be aware that you must pass in an
* actual instance of a {@link HttpClient} for this parameter so you
* cannot use directly in the Connection URL and must pass in via the
* {@link Properties} object.
* </p>
*/
public static final String PARAM_CLIENT = "client";
/**
* Static initializer block which ensures the driver gets registered
*/
static {
try {
JenaSystem.init() ;
register();
} catch (SQLException e) {
throw new RuntimeException("Failed to register Jena Remote Endpoint JDBC Driver", e);
}
}
/**
* Registers the driver with the JDBC {@link DriverManager}
*
* @throws SQLException
* Thrown if the driver cannot be registered
*/
public static synchronized void register() throws SQLException {
DriverManager.registerDriver(new RemoteEndpointDriver());
}
/**
* Creates a new driver
*/
public RemoteEndpointDriver() {
super(0, 1, REMOTE_DRIVER_PREFIX);
}
/**
* Extension point for derived drivers which allows them to provide
* different version information and driver prefix
*
* @param majorVersion
* Major version
* @param minorVersion
* Minor version
* @param driverPrefix
* Driver Prefix
*/
protected RemoteEndpointDriver(int majorVersion, int minorVersion, String driverPrefix) {
super(majorVersion, minorVersion, driverPrefix);
}
@Override
protected JenaConnection connect(Properties props, int compatibilityLevel) throws SQLException {
String queryEndpoint = props.getProperty(PARAM_QUERY_ENDPOINT);
String updateEndpoint = props.getProperty(PARAM_UPDATE_ENDPOINT);
// Validate at least one endpoint present
if (queryEndpoint == null && updateEndpoint == null)
throw new SQLException("At least one of the " + PARAM_QUERY_ENDPOINT + " or " + PARAM_UPDATE_ENDPOINT
+ " connection parameters must be specified to make a remote connection");
// Gather dataset related parameters
List<String> defaultGraphs = this.getValues(props, PARAM_DEFAULT_GRAPH_URI);
List<String> namedGraphs = this.getValues(props, PARAM_NAMED_GRAPH_URI);
List<String> usingGraphs = this.getValues(props, PARAM_USING_GRAPH_URI);
List<String> usingNamedGraphs = this.getValues(props, PARAM_USING_NAMED_GRAPH_URI);
// Authentication settings
HttpClient client = this.configureClient(props);
// Result Types
String selectResultsType = props.getProperty(PARAM_SELECT_RESULTS_TYPE, null);
String modelResultsType = props.getProperty(PARAM_MODEL_RESULTS_TYPE, null);
// Create connection
return openConnection(queryEndpoint, updateEndpoint, defaultGraphs, namedGraphs, usingGraphs, usingNamedGraphs,
client, JenaConnection.DEFAULT_HOLDABILITY, compatibilityLevel, selectResultsType, modelResultsType);
}
protected HttpClient configureClient(Properties props) throws SQLException {
// Try to get credentials to use
String user = props.getProperty(PARAM_USERNAME, null);
if (user != null && user.trim().isEmpty()) user = null;
String password = props.getProperty(PARAM_PASSWORD, null);
if (password != null && password.trim().isEmpty()) password = null;
// If credentials then we use them
if (user != null && password != null) {
BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(user, password));
return HttpClients.custom().setDefaultCredentialsProvider(credsProv).build();
}
// else use a supplied or default client
Object client = props.get(PARAM_CLIENT);
if (client != null) {
if (!(client instanceof HttpClient)) throw new SQLException("The " + PARAM_CLIENT
+ " parameter is specified but the value is not an object implementing the required HttpClient interface");
return (HttpClient) client;
}
return null;
}
/**
* Determines the common base of the two URIs if there is one. The common
* base will have irrelevant components (fragment and query string) stripped
* off of it.
* <p>
* If one URI is null and the other is non-null the non-null one is
* returned.
* </p>
*
* @param x
* URI
* @param y
* URI
* @return Common base if it exists, null otherwise.
*/
protected String getCommonBase(String x, String y) {
if (x == null) {
if (y == null) {
return null;
} else {
return stripIrrelevantComponents(y);
}
} else if (y == null) {
return stripIrrelevantComponents(x);
} else if (x.equals(y)) {
return stripIrrelevantComponents(x);
} else {
// Is one the base of the other?
if (x.length() < y.length() && y.startsWith(x)) {
return stripIrrelevantComponents(x);
} else if (y.length() < x.length() && x.startsWith(y)) {
return stripIrrelevantComponents(y);
}
// Otherwise we should strip last URI component off one/both URIs
// and recurse
if (x.length() < y.length()) {
// y is longer so strip last component
y = this.stripLastComponent(y);
} else if (x.length() > y.length()) {
// x is longer so strip last component
x = this.stripLastComponent(x);
} else {
// Equal length so strip last component from both
x = this.stripLastComponent(x);
y = this.stripLastComponent(y);
}
// Be careful that if either returned null at this point bail out
// Must do this before recursing as otherwise the recursive call
// will see one input as null and treat as if the non-null is the
// common base which is in fact incorrect in this case
if (x == null || y == null) return null;
return this.getCommonBase(x, y);
}
}
/**
* Strips the last component of the given URI if possible
*
* @param input
* URI
* @return Reduced URI or null if no further reduction is possible
*/
private String stripLastComponent(String input) {
try {
URI uri = new URI(input);
if (uri.getFragment() != null) {
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(),
null).toString();
} else if (uri.getQuery() != null) {
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null)
.toString();
} else if (uri.getPath() != null) {
// Try and strip off last segment of the path
String currPath = uri.getPath();
if (currPath.endsWith("/")) {
currPath = currPath.substring(0, currPath.length() - 1);
if (currPath.length() == 0)
currPath = null;
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), currPath, null, null)
.toString();
} else if (currPath.contains("/")) {
currPath = currPath.substring(0, currPath.lastIndexOf('/') + 1);
if (currPath.length() == 0)
currPath = null;
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), currPath, null, null)
.toString();
} else {
// If path is non-null it must always contain a /
// otherwise it would be an invalid path
// In this case there are no further components to strip
return null;
}
} else {
// No further components to strip
return null;
}
} catch (URISyntaxException e) {
// Error stripping component
return null;
}
}
/**
* Get the URI with irrelevant components (Fragment and Querystring)
* stripped off
*
* @param input
* URI
* @return URI with irrelevant components stripped off or null if stripping
* is impossible
*/
private String stripIrrelevantComponents(String input) {
try {
URI orig = new URI(input);
return new URI(orig.getScheme(), orig.getUserInfo(), orig.getHost(), orig.getPort(), orig.getPath(), null, null)
.toString();
} catch (URISyntaxException e) {
return null;
}
}
/**
* Opens the actual connection
* <p>
* This extension point allows derived drivers to return an extended version
* of a {@link RemoteEndpointConnection} if they wish or merely to leverage
* the method to provide a connection after replacing the
* {@link #connect(Properties, int)} method with their own implementation
* that uses different connection parameters
* </p>
*
* @param queryEndpoint
* SPARQL Query Endpoint
* @param updateEndpoint
* SPARQL Update Endpoint
* @param defaultGraphs
* Default Graphs for queries
* @param namedGraphs
* Named Graphs for queries
* @param usingGraphs
* Default Graphs for updates
* @param usingNamedGraphs
* Named Graphs for updates
* @param authenticator
* HTTP Authenticator
* @param holdability
* Result Set holdability
* @param compatibilityLevel
* JDBC compatibility level, see {@link JdbcCompatibility}
* @param selectResultsType
* Results Type for {@code SELECT} results
* @param modelResultsType
* Results Type for {@code CONSTRUCT} and {@code DESCRIBE}
* results
* @return Remote endpoint connection
* @throws SQLException
*/
protected RemoteEndpointConnection openConnection(String queryEndpoint, String updateEndpoint, List<String> defaultGraphs,
List<String> namedGraphs, List<String> usingGraphs, List<String> usingNamedGraphs, HttpClient client,
int holdability, int compatibilityLevel, String selectResultsType, String modelResultsType) throws SQLException {
return new RemoteEndpointConnection(queryEndpoint, updateEndpoint, defaultGraphs, namedGraphs, usingGraphs,
usingNamedGraphs, client, holdability, compatibilityLevel, selectResultsType, modelResultsType);
}
@Override
protected boolean allowsMultipleValues(String key) {
if (PARAM_DEFAULT_GRAPH_URI.equals(key) || PARAM_NAMED_GRAPH_URI.equals(key) || PARAM_USING_GRAPH_URI.equals(key)
|| PARAM_USING_NAMED_GRAPH_URI.equals(key)) {
return true;
} else {
return super.allowsMultipleValues(key);
}
}
@Override
protected DriverPropertyInfo[] getPropertyInfo(Properties connProps, List<DriverPropertyInfo> baseDriverProps) {
DriverPropertyInfo[] driverProps = new DriverPropertyInfo[10 + baseDriverProps.size()];
this.copyBaseProperties(driverProps, baseDriverProps, 10);
// Query Endpoint parameter
driverProps[0] = new DriverPropertyInfo(PARAM_QUERY_ENDPOINT, connProps.getProperty(PARAM_QUERY_ENDPOINT));
driverProps[0].required = !connProps.containsKey(PARAM_UPDATE_ENDPOINT);
driverProps[0].description = "Sets the SPARQL Query endpoint to use for query operations, if this is specified and "
+ PARAM_UPDATE_ENDPOINT + " is not then a read-only connection will be created";
// Update Endpoint parameter
driverProps[1] = new DriverPropertyInfo(PARAM_UPDATE_ENDPOINT, connProps.getProperty(PARAM_UPDATE_ENDPOINT));
driverProps[1].required = !connProps.containsKey(PARAM_UPDATE_ENDPOINT);
driverProps[1].description = "Sets the SPARQL Update endpoint to use for update operations, if this is specified and "
+ PARAM_QUERY_ENDPOINT + " is not then a write-only connection will be created";
// Default Graph parameter
driverProps[2] = new DriverPropertyInfo(PARAM_DEFAULT_GRAPH_URI, null);
driverProps[2].required = false;
driverProps[2].description = "Sets the URI for a default graph for queries, may be specified multiple times to specify multiple graphs which should form the default graph";
// Named Graph parameter
driverProps[3] = new DriverPropertyInfo(PARAM_NAMED_GRAPH_URI, null);
driverProps[3].required = false;
driverProps[3].description = "Sets the URI for a named graph for queries, may be specified multiple times to specify multiple named graphs which should be accessible";
// Using Graph parameter
driverProps[4] = new DriverPropertyInfo(PARAM_USING_GRAPH_URI, null);
driverProps[4].required = false;
driverProps[4].description = "Sets the URI for a default graph for updates, may be specified multiple times to specify multiple graphs which should form the default graph";
// Using Named Graph parameter
driverProps[5] = new DriverPropertyInfo(PARAM_USING_NAMED_GRAPH_URI, null);
driverProps[5].required = false;
driverProps[5].description = "Sets the URI for a named graph for updates, may be specified multiple times to specify multiple named graph which should be accessible";
// Results Types
driverProps[6] = new DriverPropertyInfo(PARAM_SELECT_RESULTS_TYPE, connProps.getProperty(PARAM_SELECT_RESULTS_TYPE));
driverProps[6].required = false;
driverProps[6].description = "Sets the results type for SELECT queries that will be requested from the remote endpoint";
driverProps[7] = new DriverPropertyInfo(PARAM_MODEL_RESULTS_TYPE, connProps.getProperty(PARAM_MODEL_RESULTS_TYPE));
driverProps[7].required = false;
driverProps[7].description = "Sets the results type for CONSTRUCT and DESCRIBE queries that will be requested from the remote endpoint";
// User Name parameter
driverProps[8] = new DriverPropertyInfo(PARAM_USERNAME, connProps.getProperty(PARAM_USERNAME));
// Password parameter
driverProps[9] = new DriverPropertyInfo(PARAM_PASSWORD, connProps.getProperty(PARAM_PASSWORD));
return driverProps;
}
}