/
JdbcDriver.java
265 lines (245 loc) · 10.2 KB
/
JdbcDriver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/*
* Copyright 2019 Google LLC
*
* Licensed 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 com.google.cloud.spanner.jdbc;
import com.google.api.core.InternalApi;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.connection.ConnectionOptions;
import com.google.cloud.spanner.connection.ConnectionOptions.ConnectionProperty;
import com.google.rpc.Code;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* JDBC {@link Driver} for Google Cloud Spanner.
*
* <p>Usage:
*
* <pre>
* <!--SNIPPET {@link JdbcDriver} usage-->
* {@code
* String url = "jdbc:cloudspanner:/projects/my_project_id/"
* + "instances/my_instance_id/databases/my_database_name?"
* + "credentials=/home/cloudspanner-keys/my-key.json;autocommit=false";
* try (Connection connection = DriverManager.getConnection(url)) {
* try(ResultSet rs = connection.createStatement().executeQuery("SELECT SingerId, AlbumId, MarketingBudget FROM Albums")) {
* while(rs.next()) {
* // do something
* }
* }
* }
* }
* <!--SNIPPET {@link JdbcDriver} usage-->
* </pre>
*
* The connection that is returned will implement the interface {@link CloudSpannerJdbcConnection}.
* The JDBC connection URL must be specified in the following format:
*
* <pre>
* jdbc:cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\?property-name=property-value[;property-name=property-value]*]?
* </pre>
*
* The property-value strings should be url-encoded.
*
* <p>The project-id part of the URI may be filled with the placeholder DEFAULT_PROJECT_ID. This
* placeholder is replaced by the default project id of the environment that is requesting a
* connection.
*
* <p>The supported properties are:
*
* <ul>
* <li>credentials (String): URL for the credentials file to use for the connection. If you do not
* specify any credentials at all, the default credentials of the environment as returned by
* {@link GoogleCredentials#getApplicationDefault()} is used.
* <li>oauthtoken (String): A valid OAuth2 token to use for the JDBC connection. The token must
* have been obtained with one or both of the scopes
* 'https://www.googleapis.com/auth/spanner.admin' and/or
* 'https://www.googleapis.com/auth/spanner.data'. If you specify both a credentials file and
* an OAuth token, the JDBC driver will throw an exception when you try to obtain a
* connection.
* <li>autocommit (boolean): Sets the initial autocommit mode for the connection. Default is true.
* <li>readonly (boolean): Sets the initial readonly mode for the connection. Default is false.
* <li>retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the
* connection. Default is true. @see {@link
* com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection#setRetryAbortsInternally(boolean)}
* for more information.
* <li>minSessions (int): Sets the minimum number of sessions in the backing session pool.
* Defaults to 100.
* <li>maxSessions (int): Sets the maximum number of sessions in the backing session pool.
* Defaults to 400.
* <li>numChannels (int): Sets the number of gRPC channels to use. Defaults to 4.
* <li>usePlainText (boolean): Sets whether the JDBC connection should establish an unencrypted
* connection to the server. This option can only be used when connecting to a local emulator
* that does not require an encrypted connection, and that does not require authentication.
* <li>optimizerVersion (string): The query optimizer version to use for the connection. The value
* must be either a valid version number or <code>LATEST</code>. If no value is specified, the
* query optimizer version specified in the environment variable <code>
* SPANNER_OPTIMIZER_VERSION</code> is used. If no query optimizer version is specified in the
* connection URL or in the environment variable, the default query optimizer version of Cloud
* Spanner is used.
* </ul>
*/
public class JdbcDriver implements Driver {
private static final String JDBC_API_CLIENT_LIB_TOKEN = "sp-jdbc";
static final int MAJOR_VERSION = 1;
static final int MINOR_VERSION = 0;
private static final String JDBC_URL_FORMAT =
"jdbc:" + ConnectionOptions.Builder.SPANNER_URI_FORMAT;
private static final Pattern URL_PATTERN = Pattern.compile(JDBC_URL_FORMAT);
@InternalApi
public static String getClientLibToken() {
return JDBC_API_CLIENT_LIB_TOKEN;
}
static {
try {
register();
} catch (SQLException e) {
java.sql.DriverManager.println("Registering driver failed: " + e.getMessage());
}
}
private static JdbcDriver registeredDriver;
static void register() throws SQLException {
if (isRegistered()) {
throw new IllegalStateException(
"Driver is already registered. It can only be registered once.");
}
JdbcDriver registeredDriver = new JdbcDriver();
DriverManager.registerDriver(registeredDriver);
JdbcDriver.registeredDriver = registeredDriver;
}
/**
* According to JDBC specification, this driver is registered against {@link DriverManager} when
* the class is loaded. To avoid leaks, this method allow unregistering the driver so that the
* class can be gc'ed if necessary.
*
* @throws IllegalStateException if the driver is not registered
* @throws SQLException if deregistering the driver fails
*/
static void deregister() throws SQLException {
if (!isRegistered()) {
throw new IllegalStateException(
"Driver is not registered (or it has not been registered using Driver.register() method)");
}
ConnectionOptions.closeSpanner();
DriverManager.deregisterDriver(registeredDriver);
registeredDriver = null;
}
/** @return {@code true} if the driver is registered against {@link DriverManager} */
static boolean isRegistered() {
return registeredDriver != null;
}
/**
* @return the registered JDBC driver for Cloud Spanner.
* @throws SQLException if the driver has not been registered.
*/
static JdbcDriver getRegisteredDriver() throws SQLException {
if (isRegistered()) {
return registeredDriver;
}
throw JdbcSqlExceptionFactory.of(
"The driver has not been registered", Code.FAILED_PRECONDITION);
}
public JdbcDriver() {}
@Override
public Connection connect(String url, Properties info) throws SQLException {
if (url != null && url.startsWith("jdbc:cloudspanner")) {
try {
Matcher matcher = URL_PATTERN.matcher(url);
if (matcher.matches()) {
// strip 'jdbc:' from the URL, add any extra properties and pass on to the generic
// Connection API
String connectionUri = appendPropertiesToUrl(url.substring(5), info);
ConnectionOptions options = ConnectionOptions.newBuilder().setUri(connectionUri).build();
JdbcConnection connection = new JdbcConnection(url, options);
if (options.getWarnings() != null) {
connection.pushWarning(new SQLWarning(options.getWarnings()));
}
return connection;
}
} catch (SpannerException e) {
throw JdbcSqlExceptionFactory.of(e);
} catch (IllegalArgumentException e) {
throw JdbcSqlExceptionFactory.of(e.getMessage(), Code.INVALID_ARGUMENT, e);
} catch (Exception e) {
throw JdbcSqlExceptionFactory.of(e.getMessage(), Code.UNKNOWN, e);
}
throw JdbcSqlExceptionFactory.of("invalid url: " + url, Code.INVALID_ARGUMENT);
}
return null;
}
private String appendPropertiesToUrl(String url, Properties info) {
StringBuilder res = new StringBuilder(url);
for (Entry<Object, Object> entry : info.entrySet()) {
if (entry.getValue() != null && !"".equals(entry.getValue())) {
res.append(";").append(entry.getKey()).append("=").append(entry.getValue());
}
}
return res.toString();
}
@Override
public boolean acceptsURL(String url) throws SQLException {
return URL_PATTERN.matcher(url).matches();
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
String connectionUri = appendPropertiesToUrl(url.substring(5), info);
DriverPropertyInfo[] res = new DriverPropertyInfo[ConnectionOptions.VALID_PROPERTIES.size()];
int i = 0;
for (ConnectionProperty prop : ConnectionOptions.VALID_PROPERTIES) {
res[i] =
new DriverPropertyInfo(
prop.getName(),
parseUriProperty(connectionUri, prop.getName(), prop.getDefaultValue()));
res[i].description = prop.getDescription();
res[i].choices = prop.getValidValues();
i++;
}
return res;
}
private String parseUriProperty(String uri, String property, String defaultValue) {
Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property));
Matcher matcher = pattern.matcher(uri);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return defaultValue;
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}
}