/
JdbcDriver.java
286 lines (266 loc) · 12.1 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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/*
* 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>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>autoConfigEmulator (boolean): Automatically configure the connection to try to connect to
* the Cloud Spanner emulator. You do not need to specify any host or port in the connection
* string as long as the emulator is running on the default host/port (localhost:9010). The
* instance and database in the connection string will automatically be created if these do
* not yet exist on the emulator. This means that you do not need to execute any `gcloud`
* commands on the emulator to create the instance and database before you can connect to it.
* Setting this property to true also enables running concurrent transactions on the emulator.
* The emulator aborts any concurrent transaction on the emulator, and the JDBC driver works
* around this by automatically setting a savepoint after each statement that is executed.
* When the transaction has been aborted by the emulator and the JDBC connection wants to
* continue with that transaction, the transaction is replayed up until the savepoint that had
* automatically been set after the last statement that was executed before the transaction
* was aborted by the emulator.
* <li>endpoint (string): Set this property to specify a custom endpoint that the JDBC driver
* should connect to. You can use this property in combination with the autoConfigEmulator
* property to instruct the JDBC driver to connect to an emulator instance that uses a
* randomly assigned port numer. See <a
* href="https://github.com/googleapis/java-spanner-jdbc/blob/main/src/test/java/com/google/cloud/spanner/jdbc/ConcurrentTransactionOnEmulatorTest.java">ConcurrentTransactionOnEmulatorTest</a>
* for a concrete example of how to use this property.
* <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.
* <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>retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the
* connection. Default is true. @see {@link
* 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>rpcPriority (String): Sets the priority for all RPC invocations from this connection.
* Defaults to HIGH.
* </ul>
*/
public class JdbcDriver implements Driver {
private static final String JDBC_API_CLIENT_LIB_TOKEN = "sp-jdbc";
// Updated to version 2 when upgraded to Java 8 (JDBC 4.2)
static final int MAJOR_VERSION = 2;
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) {
return URL_PATTERN.matcher(url).matches();
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
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 MAJOR_VERSION;
}
@Override
public int getMinorVersion() {
return MINOR_VERSION;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}
}