diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ef1e7a9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto eol=lf
+*.java text diff=java
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..f15fa11
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,35 @@
+name: Build and test
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: maven
+ - name: Get hyperd version
+ id: evaluate-property
+ run: |
+ echo "HYPER_VERSION=$(mvn help:evaluate -Dexpression=hyperapi.version -q -DforceStdout)" >> $GITHUB_ENV
+ - name: Cache hyperd
+ uses: actions/cache@v3
+ with:
+ path: |
+ target/.cache
+ key: ${{ runner.os }}-hyper-${{ env.HYPER_VERSION }}
+ restore-keys: |
+ ${{ runner.os }}-hyper-${{ env.HYPER_VERSION }}
+ - name: Maven package
+ run: mvn --batch-mode --no-transfer-progress clean package --file pom.xml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..68e9e88
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,42 @@
+name: Release to staging
+
+on:
+ release:
+ types: [ "created" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ server-id: ossrh
+ server-username: 'MAVEN_USERNAME'
+ server-password: 'MAVEN_PASSWORD'
+ gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }}
+ gpg-passphrase: 'MAVEN_GPG_PASSPHRASE'
+ - name: Get hyperd version
+ id: evaluate-property
+ run: |
+ echo "HYPER_VERSION=$(mvn help:evaluate -Dexpression=hyperapi.version -q -DforceStdout)" >> $GITHUB_ENV
+ - name: Cache hyperd
+ uses: actions/cache@v3
+ with:
+ path: |
+ target/.cache
+ key: ${{ runner.os }}-hyper-${{ env.HYPER_VERSION }}
+ restore-keys: |
+ ${{ runner.os }}-hyper-${{ env.HYPER_VERSION }}
+ - name: Set version
+ run: mvn versions:set --no-transfer-progress -DnewVersion=${{ github.event.release.tag_name }}
+ - name: Build with Maven
+ run: mvn --batch-mode --no-transfer-progress clean deploy -P release --file pom.xml
+ env:
+ MAVEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSWORD }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d467b07
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+.idea
+!.idea/externalDependencies.xml
+!.idea/palantir-java-format.xml
+
+.DS_Store
+
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+.project
+.classpath
+src/main/resources/config/config.properties
+
+*.iml
+pom.xml.bak
diff --git a/.hooks/pre-commit b/.hooks/pre-commit
new file mode 100755
index 0000000..8c878af
--- /dev/null
+++ b/.hooks/pre-commit
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+echo '[git pre-commit] mvn spotless:apply sortpom:sort'
+MAVEN_OPTS='-Dorg.slf4j.simpleLogger.defaultLogLevel=error' mvn spotless:apply sortpom:sort
+git add --update
\ No newline at end of file
diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml
new file mode 100644
index 0000000..faf3cba
--- /dev/null
+++ b/.idea/externalDependencies.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/palantir-java-format.xml b/.idea/palantir-java-format.xml
new file mode 100644
index 0000000..3815718
--- /dev/null
+++ b/.idea/palantir-java-format.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
index 6010d8c..0393ef9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,3 +1,4 @@
# Comment line immediately above ownership line is reserved for related other information. Please be careful while editing.
-#ECCN:Open Source
-#GUSINFO:Open Source,Open Source Workflow
\ No newline at end of file
+#ECCN: 5D002.c.1
+#GUSINFO:Open Source,Open Source Workflow
+* datacloud-query-connector-owners@salesforce.com
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9bdfbf2..2d2ecb9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,46 +1,27 @@
-*This is a suggested `CONTRIBUTING.md` file template for use by open sourced Salesforce projects. The main goal of this file is to make clear the intents and expectations that end-users may have regarding this project and how/if to engage with it. Adjust as needed (especially look for `{project_slug}` which refers to the org and repo name of your project) and remove this paragraph before committing to your repo.*
+# Contributing Guide For Data Cloud JDBC Driver
-# Contributing Guide For {NAME OF PROJECT}
-
-This page lists the operational governance model of this project, as well as the recommendations and requirements for how to best contribute to {PROJECT}. We strive to obey these as best as possible. As always, thanks for contributing – we hope these guidelines make it easier and shed some light on our approach and processes.
+This page lists the operational governance model of this project, as well as the recommendations and requirements for how to best contribute to Data Cloud JDBC Driver. We strive to obey these as best as possible. As always, thanks for contributing – we hope these guidelines make it easier and shed some light on our approach and processes.
# Governance Model
-> Pick the most appropriate one
-
-## Community Based
-
-The intent and goal of open sourcing this project is to increase the contributor and user base. The governance model is one where new project leads (`admins`) will be added to the project based on their contributions and efforts, a so-called "do-acracy" or "meritocracy" similar to that used by all Apache Software Foundation projects.
-
-> or
## Salesforce Sponsored
The intent and goal of open sourcing this project is to increase the contributor and user base. However, only Salesforce employees will be given `admin` rights and will be the final arbitrars of what contributions are accepted or not.
-> or
-
-## Published but not supported
-
-The intent and goal of open sourcing this project is because it may contain useful or interesting code/concepts that we wish to share with the larger open source community. Although occasional work may be done on it, we will not be looking for or soliciting contributions.
-
-# Getting started
-
-Please join the community on {Here list Slack channels, Email lists, Glitter, Discord, etc... links}. Also please make sure to take a look at the project [roadmap](ROADMAP.md) to see where are headed.
-
# Issues, requests & ideas
Use GitHub Issues page to submit issues, enhancement requests and discuss ideas.
### Bug Reports and Fixes
-- If you find a bug, please search for it in the [Issues](https://github.com/{project_slug}/issues), and if it isn't already tracked,
- [create a new issue](https://github.com/{project_slug}/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still
+- If you find a bug, please search for it in the [Issues](https://github.com/forcedotcom/datacloud-jdbc/issues), and if it isn't already tracked,
+ [create a new issue](https://github.com/forcedotcom/datacloud-jdbc/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still
be reviewed.
- Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`.
- If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number.
- Include tests that isolate the bug and verifies that it was fixed.
### New Features
-- If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/{project_slug}/issues/new).
+- If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/forcedotcom/datacloud-jdbc/issues/new).
- Issues that have been identified as a feature request will be labelled `enhancement`.
- If you'd like to implement the new feature, please wait for feedback from the project
maintainers before spending too much time writing the code. In some cases, `enhancement`s may
@@ -51,7 +32,7 @@ Use GitHub Issues page to submit issues, enhancement requests and discuss ideas.
alternative implementation of something that may have advantages over the way its currently
done, or you have any other change, we would be happy to hear about it!
- If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind.
- - If not, [open an Issue](https://github.com/{project_slug}/issues/new) to discuss the idea first.
+ - If not, [open an Issue](https://github.com/forcedotcom/datacloud-jdbc/issues/new) to discuss the idea first.
If you're new to our project and looking for some way to make your first contribution, look for
Issues labelled `good first contribution`.
diff --git a/LICENSE.txt b/LICENSE.txt
index ae7332a..c2516fc 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -191,7 +191,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright {yyyy} {name of copyright owner}
+ Copyright 2024 Salesforce
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index b2beac8..4164c31 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,178 @@
-# README
+# Salesforce DataCloud JDBC Driver
-A repo containing all the basic file templates and general guidelines for any open source project at Salesforce.
+With the Salesforce Data Cloud JDBC driver you can efficiently query millions of rows of data with low latency, and perform bulk data extractions.
+This driver is read-only and forward-only.
+It requires Java 11 or greater.
+
+
+## Getting started
+
+To add the driver to your project, add the following Maven dependency:
+
+```xml
+
+ com.salesforce.datacloud
+ jdbc
+ ${jdbc.version}
+
+```
+
+The class name for this driver is:
+
+```
+com.salesforce.datacloud.jdbc.DataCloudJDBCDriver
+```
+
+## Building the driver:
+
+Use the following command to build and test the driver:
+
+```shell
+mvn clean install
+```
## Usage
-It's required that all files must be placed at the top level of your repository.
+### Connection string
+
+Use `jdbc:salesforce-datacloud://login.salesforce.com`
+
+### JDBC Driver class
+
+Use `com.salesforce.datacloud.jdbc.DataCloudJDBCDriver` as the driver class name for the JDBC application.
+
+### Authentication
+
+We support three of the [OAuth authorization flows][oauth authorization flows] provided by Salesforce.
+All of these flows require a connected app be configured for the driver to authenticate as, see the documentation here: [connected app overview][connected app overview].
+Set the following properties appropriately to establish a connection with your chosen OAuth authorization flow:
+
+| Parameter | Description |
+|--------------|----------------------------------------------------------------------------------------------------------------------|
+| user | The login name of the user. |
+| password | The password of the user. |
+| clientId | The consumer key of the connected app. |
+| clientSecret | The consumer secret of the connected app. |
+| privateKey | The private key of the connected app. |
+| coreToken | OAuth token that a connected app uses to request access to a protected resource on behalf of the client application. |
+| refreshToken | Token obtained from the web server, user-agent, or hybrid app token flow. |
+
+
+#### username and password authentication:
+
+The documentation for username and password authentication can be found [here][username flow].
+
+To configure username and password, set properties like so:
+
+```java
+Properties properties = new Properties();
+properties.put("user", "${userName}");
+properties.put("password", "${password}");
+properties.put("clientId", "${clientId}");
+properties.put("clientSecret", "${clientSecret}");
+```
+
+#### jwt authentication:
+
+The documentation for jwt authentication can be found [here][jwt flow].
+
+Instuctions to generate a private key can be found [here](#generating-a-private-key-for-jwt-authentication)
+
+```java
+Properties properties = new Properties();
+properties.put("privateKey", "${privateKey}");
+properties.put("clientId", "${clientId}");
+properties.put("clientSecret", "${clientSecret}");
+```
+
+#### refresh token authentication:
+
+The documentation for refresh token authentication can be found [here][refresh token flow].
+
+```java
+Properties properties = new Properties();
+properties.put("coreToken", "${coreToken}");
+properties.put("refreshToken", "${refreshToken}");
+properties.put("clientId", "${clientId}");
+properties.put("clientSecret", "${clientSecret}");
+```
+
+### Connection settings
+
+See this page on available [connection settings][connection settings].
+These settings can be configured in properties by using the prefix `serverSetting.`
+
+For example, to control locale set the following property:
+
+```java
+properties.put("serverSetting.lc_time", "en_US");
+```
+
+---
+
+### Generating a private key for jwt authentication
+
+To authenticate using key-pair authentication you'll need to generate a certificate and register it with your connected app.
+
+```shell
+# create a key pair:
+openssl genrsa -out keypair.key 2048
+# create a digital certificate, follow the prompts:
+openssl req -new -x509 -nodes -sha256 -days 365 -key keypair.key -out certificate.crt
+# create a private key from the key pair:
+openssl pkcs8 -topk8 -nocrypt -in keypair.key -out private.key
+```
+
+### Optional configuration
+
+- `dataspace`: The data space to query, defaults to "default"
+- `User-Agent`: The User-Agent string identifies the JDBC driver and, optionally, the client application making the database connection.
+ By default, the User-Agent string will end with "salesforce-datacloud-jdbc/{version}" and we will prepend any User-Agent provided by the client application.
+ For example: "User-Agent: ClientApp/1.2.3 salesforce-datacloud-jdbc/1.0"
+
+
+### Usage sample code
+
+```java
+public static void executeQuery() throws ClassNotFoundException, SQLException {
+ Class.forName("com.salesforce.datacloud.jdbc.DataCloudJDBCDriver");
+
+ Properties properties = new Properties();
+ properties.put("user", "${userName}");
+ properties.put("password", "${password}");
+ properties.put("clientId", "${clientId}");
+ properties.put("clientSecret", "${clientSecret}");
+
+ try (var connection = DriverManager.getConnection("jdbc:salesforce-datacloud://login.salesforce.com", properties);
+ var statement = connection.createStatement()) {
+ var resultSet = statement.executeQuery("${query}");
+
+ while (resultSet.next()) {
+ // Iterate over the result set
+ }
+ }
+}
+```
+
+## Generated assertions
+
+Some of our classes are tested using assertions generated with [the assertj assertions generator][assertion generator].
+Due to some transient test-compile issues we experienced, we checked in generated assertions for some of our classes.
+If you make changes to any of these classes, you will need to re-run the assertion generator to have the appropriate assertions available for that class.
+
+To find examples of these generated assertions, look for files with the path `**/test/**/*Assert.java`.
+
+To re-generate these assertions execute the following command:
+
+```shell
+mvn assertj:generate-assertions
+```
-> **NOTE** Your README should contain detailed, useful information about the project!
+[oauth authorization flows]: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_flows.htm&type=5
+[username flow]: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_username_password_flow.htm&type=5
+[jwt flow]: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5
+[refresh token flow]: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_refresh_token_flow.htm&type=5
+[connection settings]: https://tableau.github.io/hyper-db/docs/hyper-api/connection#connection-settings
+[assertion generator]: https://joel-costigliola.github.io/assertj/assertj-assertions-generator-maven-plugin.html#configuration
+[connected app overview]: https://help.salesforce.com/s/articleView?id=sf.connected_app_overview.htm&type=5
\ No newline at end of file
diff --git a/license-header.txt b/license-header.txt
new file mode 100644
index 0000000..2ff21cf
--- /dev/null
+++ b/license-header.txt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.
+ */
\ No newline at end of file
diff --git a/license_info.md b/license_info.md
deleted file mode 100644
index b8da254..0000000
--- a/license_info.md
+++ /dev/null
@@ -1,224 +0,0 @@
-License Info
-------------
-
-Most projects we open source should use the [Apache License v2](https://opensource.org/license/apache-2-0/) license. Samples, demos, and blog / doc code examples should instead use [CC-0](https://creativecommons.org/publicdomain/zero/1.0/). If you strongly feel your project should perhaps use a different license clause, please engage with legal team.
-
-For the ALv2 license, create a `LICENSE.txt` file (or use the one in this template repo) in the root of your repo containing:
-```
-Apache License Version 2.0
-
-Copyright (c) 2023 Salesforce, Inc.
-All rights reserved.
-
-Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-```
-
-The shorter version of license text should be added as a comment to all Salesforce-authored source code and configuration files that support comments. This include file formats like HTML, CSS, JavaScript, XML, etc. which aren't directly code, but are still critical to your project code. Like:
-```
-/*
- * Copyright (c) 2023, Salesforce, Inc.
- * SPDX-License-Identifier: Apache-2
- *
- * 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.
- */
- ```
-
-Note that there are many tools that exist to do this sort of thing in an automated fashion, without having to manually edit every single file in your project. It is highly recommended that you research some of these tools for your particular language / build system.
-
-For sample, demo, and example code, we recommend the [Unlicense](https://opensource.org/license/unlicense/) license. Create a `LICENSE.txt` file containing:
-```
-This is free and unencumbered software released into the public domain.
-
-Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
-
-In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
-
-THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-```
-
-No license header is required for samples, demos, and example code.
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 0000000..be5091c
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,3 @@
+config.stopBubbling = true
+lombok.nonNull.exceptionType = IllegalArgumentException
+lombok.addLombokGeneratedAnnotation = true
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..dab03f8
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,819 @@
+
+
+ 4.0.0
+ com.salesforce.datacloud
+ jdbc
+ 0.20.0-SNAPSHOT
+ jar
+ Salesforce Data Cloud JDBC Driver
+ Salesforce Data Cloud JDBC Driver
+
+ 18.0.0
+ 3.26.3
+ 1.25.0
+ 1.17.1
+ 3.17.0
+ ${project.build.directory}/.cache
+ 3.5.0
+ 0.13.0
+
+ 0.0.20746.reac9bd2d
+ ${project.build.directory}/hyper
+ 2.18.0
+ 0.8.12
+ 11
+ 0.12.6
+ 5.11.3
+ 1.18.34
+ {java.version}
+ {java.version}
+ 5.14.1
+ 4.12.0
+ UTF-8
+ UTF-8
+ 3.25.5
+ com.salesforce.datacloud.jdbc.internal.shaded
+ 1.7.32
+ 2.43.0
+
+
+
+
+ org.apache.arrow
+ arrow-bom
+ ${arrow.version}
+ pom
+ import
+
+
+ org.junit
+ junit-bom
+ ${junit-bom.version}
+ pom
+ import
+
+
+ org.mockito
+ mockito-bom
+ ${mockito-bom.version}
+ pom
+ import
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf-java.version}
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
+
+ commons-cli
+ commons-cli
+ 1.6.0
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec.version}
+
+
+ io.grpc
+ grpc-netty-shaded
+ 1.63.0
+
+
+ io.grpc
+ grpc-protobuf
+ 1.63.0
+
+
+ io.grpc
+ grpc-stub
+ 1.63.0
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+
+
+ net.jodah
+ failsafe
+ 2.4.4
+
+
+ org.apache.arrow
+ arrow-vector
+
+
+ org.apache.calcite.avatica
+ avatica
+ ${avatica.version}
+
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ io.jsonwebtoken
+ jjwt-gson
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ org.apache.arrow
+ arrow-memory-netty
+ runtime
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ ${okhttp.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+ org.grpcmock
+ grpcmock-junit5
+ ${grpcmock.junit5.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+
+
+
+ true
+ src/main/resources
+
+
+ true
+ src/test/resources
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+ com.google.protobuf:protoc:3.19.6:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:1.63.0:exe:${os.detected.classifier}
+ false
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.6.0
+
+ true
+ shaded
+ jdbc-shaded
+ false
+
+
+ org.apache
+ ${shadeBase}.apache
+
+
+ io.netty
+ ${shadeBase}.io.netty
+
+
+ io.grpc
+ ${shadeBase}.io.grpc
+
+
+ com.fasterxml.jackson
+ ${shadeBase}.com.fasterxml.jackson
+
+
+ io.jsonwebtoken
+ ${shadeBase}.io.jsonwebtoken
+
+
+ io.vavr
+ ${shadeBase}.io.vavr
+
+
+ com.squareup
+ ${shadeBase}.com.squareup
+
+
+ com.google
+ ${shadeBase}.com.google
+
+
+ net.jodah
+ ${shadeBase}.net.jodah
+
+
+ org.projectlombok
+ ${shadeBase}.org.projectlombok
+
+
+ javax.annotation
+ ${shadeBase}.javax.annotation
+
+
+ com.google.protobuf
+ ${shadeBase}.com.google.protobuf
+
+
+ commons-cli
+ ${shadeBase}.commons-cli
+
+
+ commons-codec
+ ${shadeBase}.commons-codec
+
+
+ org.slf4j
+ ${shadeBase}.org.slf4j
+
+
+
+
+ *:*
+
+ META-INF/LICENSE*
+ META-INF/NOTICE*
+ META-INF/DEPENDENCIES
+ META-INF/maven/**
+ META-INF/services/com.fasterxml.*
+ META-INF/*.xml
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+ .netbeans_automatic_build
+ git.properties
+ google-http-client.properties
+ storage.v1.json
+
+ pipes-fork-server-default-log4j2.xml
+ dependencies.properties
+ pipes-fork-server-default-log4j2.xml
+
+
+
+ org.apache.arrow:arrow-vector
+
+
+ codegen/**
+
+
+
+ org.slf4j:slf4j-simple
+
+
+ org/slf4j/**
+
+
+
+
+
+
+
+
+
+
+
+ shade
+
+ package
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ ${java.version}
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.7.1
+
+ true
+ true
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+
+ single
+
+ package
+
+
+
+
+ org.projectlombok
+ lombok-maven-plugin
+ 1.18.20.0
+
+
+ delombok
+
+ delombok
+
+
+ false
+ ${project.basedir}/src/main/java
+ ${project.build.directory}/delombok
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.1
+
+ ${project.build.directory}/delombok;${project.build.directory}/generated-sources/protobuf
+
+ true
+ com.salesforce.hyperdb.grpc
+ ${project.build.directory}/apidocs
+
+
+
+
+ jar
+
+ package
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+
+ jar
+
+ package
+
+
+
+
+ maven-surefire-plugin
+ 3.3.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-bom.version}
+
+
+
+
+ maven-failsafe-plugin
+ 3.3.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-bom.version}
+
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.maven.plugin.version}
+
+
+
+ prepare-agent
+
+
+
+ report
+
+ report
+
+ test
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ ${spotless.version}
+
+ origin/main
+ ${release.profile.active}
+
+
+
+ .gitattributes
+ .gitignore
+
+
+
+
+ true
+ 4
+
+
+
+
+
+
+ src/main/java/**/*.java
+ src/test/java/**/*.java
+
+
+ 2.39.0
+
+ true
+
+
+
+
+
+ ${project.basedir}/license-header.txt
+
+
+
+
+
+
+ check
+ apply
+
+
+
+
+
+ com.rudikershaw.gitbuildhook
+ git-build-hook-maven-plugin
+ ${git-build-hook-maven-plugin.version}
+
+
+ .hooks/pre-commit
+
+
+
+
+
+ install
+
+
+
+
+
+ org.assertj
+ assertj-assertions-generator-maven-plugin
+ 2.2.0
+
+
+ com.salesforce.datacloud.jdbc.core.accessor.QueryJDBCAccessor
+
+ false
+ src/test/java
+ false
+ true
+ false
+ false
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.2.0
+
+
+ ${*}
+
+
+
+
+ com.github.ekryd.sortpom
+ sortpom-maven-plugin
+ 3.4.1
+
+ ${project.build.sourceEncoding}
+ custom_1
+ 4
+ \n
+ scope,groupId,artifactId
+ true
+ true
+ false
+ stop
+
+
+
+
+ sort
+
+ generate-sources
+
+
+ verify-sorted-pom
+
+ verify
+
+ validate
+
+
+
+
+ com.googlecode.maven-download-plugin
+ download-maven-plugin
+ 1.11.3
+
+
+ download-hyper-cpp
+
+ wget
+
+ process-test-resources
+
+ ${hyper-download-url}.${hyperapi.version}.zip
+ true
+ ${download.cache.directory}/hyper-unzipped
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ flatten-hyperd
+
+ run
+
+ process-test-resources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ chmod-hyperd
+
+ run
+
+ process-test-resources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+ https://github.com/forcedotcom/datacloud-jdbc
+
+
+ Apache License Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ Data Cloud Query Developer Team
+ datacloud-query-connector-owners@salesforce.com
+ Salesforce Data Cloud
+ https://www.salesforce.com/data/
+
+
+
+ scm:git:https://github.com/forcedotcom/datacloud-jdbc.git
+ scm:git:git@github.com:forcedotcom/datacloud-jdbc.git
+ https://github.com/forcedotcom/datacloud-jdbc
+
+
+ GitHub Issues
+ https://github.com/forcedotcom/datacloud-jdbc/issues
+
+
+ GitHub Actions
+ https://github.com/forcedotcom/datacloud-jdbc/actions
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+ windows
+
+
+ windows
+
+
+
+
+ https://downloads.tableau.com/tssoftware/tableauhyperapi-cxx-windows-x86_64-release-main
+
+
+
+ mac-apple-silicon
+
+
+ mac
+ aarch64
+
+
+
+
+ https://downloads.tableau.com/tssoftware/tableauhyperapi-cxx-macos-arm64-release-main
+
+
+
+ mac-x86_64
+
+
+ mac
+ x86_64
+
+
+
+
+ https://downloads.tableau.com/tssoftware//tableauhyperapi-cxx-macos-x86_64-release-main
+
+
+
+ linux
+
+
+ !mac os x
+ unix
+
+
+
+
+ https://downloads.tableau.com/tssoftware/tableauhyperapi-cxx-linux-x86_64-release-main
+
+
+
+ release
+
+ true
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.7.0
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+ sign-artifacts
+
+ sign
+
+ verify
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/DataCloudDatasource.java b/src/main/java/com/salesforce/datacloud/jdbc/DataCloudDatasource.java
new file mode 100644
index 0000000..1a083c4
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/DataCloudDatasource.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc;
+
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.util.SqlErrorCodes;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.logging.Logger;
+import javax.sql.DataSource;
+import lombok.SneakyThrows;
+
+public class DataCloudDatasource implements DataSource {
+ private static final String USERNAME_PROPERTY = "userName";
+ private static final String PASSWORD_PROPERTY = "password";
+ private static final String PRIVATE_KEY_PROPERTY = "privateKey";
+ private static final String REFRESH_TOKEN_PROPERTY = "refreshToken";
+ private static final String CORE_TOKEN_PROPERTY = "coreToken";
+ private static final String CLIENT_ID_PROPERTY = "clientId";
+ private static final String CLIENT_SECRET_PROPERTY = "clientSecret";
+ private static final String INTERNAL_ENDPOINT_PROPERTY = "internalEndpoint";
+ private static final String PORT_PROPERTY = "port";
+ private static final String TENANT_ID_PROPERTY = "tenantId";
+ private static final String DATASPACE_PROPERTY = "dataspace";
+ private static final String CORE_TENANT_ID_PROPERTY = "coreTenantId";
+
+ protected static final String NOT_SUPPORTED_IN_DATACLOUD_QUERY =
+ "Datasource method is not supported in Data Cloud query";
+
+ private String connectionUrl;
+ private final Properties properties = new Properties();
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ try {
+ return DriverManager.getConnection(getConnectionUrl(), properties);
+ } catch (SQLException e) {
+ throw new DataCloudJDBCException(e);
+ }
+ }
+
+ @Override
+ public Connection getConnection(String username, String password) throws SQLException {
+ setUserName(username);
+ setPassword(password);
+ return getConnection();
+ }
+
+ @Override
+ public PrintWriter getLogWriter() throws SQLException {
+ throw new DataCloudJDBCException(NOT_SUPPORTED_IN_DATACLOUD_QUERY, SqlErrorCodes.FEATURE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public void setLogWriter(PrintWriter out) throws SQLException {
+ throw new DataCloudJDBCException(NOT_SUPPORTED_IN_DATACLOUD_QUERY, SqlErrorCodes.FEATURE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ throw new DataCloudJDBCException(NOT_SUPPORTED_IN_DATACLOUD_QUERY, SqlErrorCodes.FEATURE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ throw new DataCloudJDBCException(NOT_SUPPORTED_IN_DATACLOUD_QUERY, SqlErrorCodes.FEATURE_NOT_SUPPORTED);
+ }
+
+ @SneakyThrows
+ @Override
+ public Logger getParentLogger() {
+ throw new DataCloudJDBCException(NOT_SUPPORTED_IN_DATACLOUD_QUERY, SqlErrorCodes.FEATURE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ return null;
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return false;
+ }
+
+ private String getConnectionUrl() {
+ return connectionUrl;
+ }
+
+ public void setConnectionUrl(String connectionUrl) {
+ this.connectionUrl = connectionUrl;
+ }
+
+ public void setUserName(String userName) {
+ this.properties.setProperty(USERNAME_PROPERTY, userName);
+ }
+
+ public void setPassword(String password) {
+ this.properties.setProperty(PASSWORD_PROPERTY, password);
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.properties.setProperty(PRIVATE_KEY_PROPERTY, privateKey);
+ }
+
+ public void setRefreshToken(String refreshToken) {
+ this.properties.setProperty(REFRESH_TOKEN_PROPERTY, refreshToken);
+ }
+
+ public void setCoreToken(String coreToken) {
+ this.properties.setProperty(CORE_TOKEN_PROPERTY, coreToken);
+ }
+
+ public void setInternalEndpoint(String internalEndpoint) {
+ this.properties.setProperty(INTERNAL_ENDPOINT_PROPERTY, internalEndpoint);
+ }
+
+ public void setPort(String port) {
+ this.properties.setProperty(PORT_PROPERTY, port);
+ }
+
+ public void setTenantId(String tenantId) {
+ this.properties.setProperty(TENANT_ID_PROPERTY, tenantId);
+ }
+
+ public void setDataspace(String dataspace) {
+ this.properties.setProperty(DATASPACE_PROPERTY, dataspace);
+ }
+
+ public void setCoreTenantId(String coreTenantId) {
+ this.properties.setProperty(CORE_TENANT_ID_PROPERTY, coreTenantId);
+ }
+
+ public void setClientId(String clientId) {
+ this.properties.setProperty(CLIENT_ID_PROPERTY, clientId);
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.properties.setProperty(CLIENT_SECRET_PROPERTY, clientSecret);
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/DataCloudJDBCDriver.java b/src/main/java/com/salesforce/datacloud/jdbc/DataCloudJDBCDriver.java
new file mode 100644
index 0000000..eb001eb
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/DataCloudJDBCDriver.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc;
+
+import com.salesforce.datacloud.jdbc.config.DriverVersion;
+import com.salesforce.datacloud.jdbc.core.DataCloudConnection;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.DriverPropertyInfo;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.logging.Logger;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class DataCloudJDBCDriver implements Driver {
+ private static Driver registeredDriver;
+
+ static {
+ try {
+ register();
+ log.info("DataCloud JDBC driver registered");
+ } catch (SQLException e) {
+ log.error("Error occurred while registering DataCloud JDBC driver. {}", e.getMessage());
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static void register() throws SQLException {
+ if (isRegistered()) {
+ throw new IllegalStateException("Driver is already registered. It can only be registered once.");
+ }
+ registeredDriver = new DataCloudJDBCDriver();
+ DriverManager.registerDriver(registeredDriver);
+ }
+
+ public static boolean isRegistered() {
+ return registeredDriver != null;
+ }
+
+ @Override
+ public Connection connect(String url, Properties info) throws SQLException {
+ if (url == null) {
+ throw new SQLException("Error occurred while registering JDBC driver. URL is null.");
+ }
+
+ if (!this.acceptsURL(url)) {
+ return null;
+ }
+ return DataCloudConnection.of(url, info);
+ }
+
+ @Override
+ public boolean acceptsURL(String url) {
+ return DataCloudConnection.acceptsUrl(url);
+ }
+
+ @Override
+ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
+ return new DriverPropertyInfo[0];
+ }
+
+ @Override
+ public int getMajorVersion() {
+ return DriverVersion.getMajorVersion();
+ }
+
+ @Override
+ public int getMinorVersion() {
+ return DriverVersion.getMinorVersion();
+ }
+
+ @Override
+ public boolean jdbcCompliant() {
+ return false;
+ }
+
+ @Override
+ public Logger getParentLogger() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/AuthenticationSettings.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/AuthenticationSettings.java
new file mode 100644
index 0000000..6d70aac
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/AuthenticationSettings.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import static com.salesforce.datacloud.jdbc.util.PropertiesExtensions.copy;
+import static com.salesforce.datacloud.jdbc.util.PropertiesExtensions.optional;
+import static com.salesforce.datacloud.jdbc.util.PropertiesExtensions.required;
+
+import com.salesforce.datacloud.jdbc.config.DriverVersion;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.util.PropertiesExtensions;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+import lombok.val;
+
+@Getter
+public abstract class AuthenticationSettings {
+ public static AuthenticationSettings of(@NonNull Properties properties) throws SQLException {
+ checkNotEmpty(properties);
+ checkHasAllRequired(properties);
+
+ if (hasPrivateKey(properties)) {
+ return new PrivateKeyAuthenticationSettings(properties);
+ } else if (hasPassword(properties)) {
+ return new PasswordAuthenticationSettings(properties);
+ } else if (hasRefreshToken(properties)) {
+ return new RefreshTokenAuthenticationSettings(properties);
+ } else {
+ throw new DataCloudJDBCException(Messages.PROPERTIES_MISSING, "28000");
+ }
+ }
+
+ public static boolean hasAll(Properties properties, Set keys) {
+ return keys.stream().allMatch(k -> optional(properties, k).isPresent());
+ }
+
+ public static boolean hasAny(Properties properties) {
+ return hasPrivateKey(properties) || hasPassword(properties) || hasRefreshToken(properties);
+ }
+
+ private static boolean hasPrivateKey(Properties properties) {
+ return hasAll(properties, Keys.PRIVATE_KEY_KEYS);
+ }
+
+ private static boolean hasPassword(Properties properties) {
+ return hasAll(properties, Keys.PASSWORD_KEYS);
+ }
+
+ private static boolean hasRefreshToken(Properties properties) {
+ return hasAll(properties, Keys.REFRESH_TOKEN_KEYS);
+ }
+
+ private static void checkNotEmpty(@NonNull Properties properties) throws SQLException {
+ if (properties.isEmpty()) {
+ throw new DataCloudJDBCException(
+ Messages.PROPERTIES_EMPTY, "28000", new IllegalArgumentException(Messages.PROPERTIES_EMPTY));
+ }
+ }
+
+ private static void checkHasAllRequired(Properties properties) throws SQLException {
+ if (hasAll(properties, Keys.REQUIRED_KEYS)) {
+ return;
+ }
+
+ val missing = Keys.REQUIRED_KEYS.stream()
+ .filter(k -> optional(properties, k).isEmpty())
+ .collect(Collectors.joining(", ", Messages.PROPERTIES_REQUIRED, ""));
+
+ throw new DataCloudJDBCException(missing, "28000", new IllegalArgumentException(missing));
+ }
+
+ final URI getLoginUri() throws SQLException {
+ try {
+ return new URI(loginUrl);
+ } catch (URISyntaxException ex) {
+ throw new DataCloudJDBCException(ex.getMessage(), "28000", ex);
+ }
+ }
+
+ protected AuthenticationSettings(@NonNull Properties properties) throws SQLException {
+ checkNotEmpty(properties);
+
+ this.relevantProperties = copy(properties, Keys.ALL);
+
+ this.loginUrl = required(relevantProperties, Keys.LOGIN_URL);
+ this.clientId = required(relevantProperties, Keys.CLIENT_ID);
+ this.clientSecret = required(relevantProperties, Keys.CLIENT_SECRET);
+
+ this.dataspace = optional(relevantProperties, Keys.DATASPACE).orElse(Defaults.DATASPACE);
+ this.userAgent = optional(relevantProperties, Keys.USER_AGENT).orElse(Defaults.USER_AGENT);
+ this.maxRetries = optional(relevantProperties, Keys.MAX_RETRIES)
+ .map(PropertiesExtensions::toIntegerOrNull)
+ .orElse(Defaults.MAX_RETRIES);
+ }
+
+ private final Properties relevantProperties;
+
+ private final String loginUrl;
+ private final String clientId;
+ private final String clientSecret;
+ private final String dataspace;
+ private final String userAgent;
+ private final int maxRetries;
+
+ @UtilityClass
+ protected static class Keys {
+ static final String LOGIN_URL = "loginURL";
+ static final String USER_NAME = "userName";
+ static final String PASSWORD = "password";
+ static final String PRIVATE_KEY = "privateKey";
+ static final String CLIENT_SECRET = "clientSecret";
+ static final String CLIENT_ID = "clientId";
+ static final String DATASPACE = "dataspace";
+ static final String MAX_RETRIES = "maxRetries";
+ static final String USER_AGENT = "User-Agent";
+ static final String REFRESH_TOKEN = "refreshToken";
+
+ static final Set REQUIRED_KEYS = Set.of(LOGIN_URL, CLIENT_ID, CLIENT_SECRET);
+
+ static final Set OPTIONAL_KEYS = Set.of(DATASPACE, USER_AGENT, MAX_RETRIES);
+
+ static final Set PASSWORD_KEYS = Set.of(USER_NAME, PASSWORD);
+
+ static final Set PRIVATE_KEY_KEYS = Set.of(USER_NAME, PRIVATE_KEY);
+
+ static final Set REFRESH_TOKEN_KEYS = Set.of(REFRESH_TOKEN);
+
+ static final Set ALL = Stream.of(
+ REQUIRED_KEYS, OPTIONAL_KEYS, PASSWORD_KEYS, PRIVATE_KEY_KEYS, REFRESH_TOKEN_KEYS)
+ .flatMap(Set::stream)
+ .collect(Collectors.toSet());
+ }
+
+ @UtilityClass
+ protected static class Defaults {
+ static final int MAX_RETRIES = 3;
+ static final String DATASPACE = null;
+ static final String USER_AGENT = DriverVersion.formatDriverInfo();
+ }
+
+ @UtilityClass
+ protected static class Messages {
+ static final String PROPERTIES_NULL = "properties is marked non-null but is null";
+ static final String PROPERTIES_EMPTY = "Properties cannot be empty when creating AuthenticationSettings.";
+ static final String PROPERTIES_MISSING =
+ "Properties did not contain valid settings for known authentication strategies: password, privateKey, or refreshToken with coreToken";
+ static final String PROPERTIES_REQUIRED = "Properties did not contain the following required settings: ";
+ }
+}
+
+@Getter
+class PasswordAuthenticationSettings extends AuthenticationSettings {
+ protected PasswordAuthenticationSettings(@NonNull Properties properties) throws SQLException {
+ super(properties);
+
+ this.password = required(this.getRelevantProperties(), Keys.PASSWORD);
+ this.userName = required(this.getRelevantProperties(), Keys.USER_NAME);
+ }
+
+ private final String password;
+ private final String userName;
+}
+
+@Getter
+class PrivateKeyAuthenticationSettings extends AuthenticationSettings {
+ protected PrivateKeyAuthenticationSettings(@NonNull Properties properties) throws SQLException {
+ super(properties);
+
+ this.privateKey = required(this.getRelevantProperties(), Keys.PRIVATE_KEY);
+ this.userName = required(this.getRelevantProperties(), Keys.USER_NAME);
+ }
+
+ private final String privateKey;
+ private final String userName;
+}
+
+@Getter
+class RefreshTokenAuthenticationSettings extends AuthenticationSettings {
+ protected RefreshTokenAuthenticationSettings(@NonNull Properties properties) throws SQLException {
+ super(properties);
+
+ this.refreshToken = required(this.getRelevantProperties(), Keys.REFRESH_TOKEN);
+ }
+
+ private final String refreshToken;
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/AuthenticationStrategy.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/AuthenticationStrategy.java
new file mode 100644
index 0000000..0a79194
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/AuthenticationStrategy.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.http.FormCommand;
+import java.net.URI;
+import java.sql.SQLException;
+import java.util.Properties;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+import lombok.val;
+import org.apache.commons.lang3.StringUtils;
+
+interface AuthenticationStrategy {
+ static AuthenticationStrategy of(@NonNull Properties properties) throws SQLException {
+ val settings = AuthenticationSettings.of(properties);
+ return of(settings);
+ }
+
+ static AuthenticationStrategy of(@NonNull AuthenticationSettings settings) throws SQLException {
+ if (settings instanceof PasswordAuthenticationSettings) {
+ return new PasswordAuthenticationStrategy((PasswordAuthenticationSettings) settings);
+ } else if (settings instanceof PrivateKeyAuthenticationSettings) {
+ return new PrivateKeyAuthenticationStrategy((PrivateKeyAuthenticationSettings) settings);
+ } else if (settings instanceof RefreshTokenAuthenticationSettings) {
+ return new RefreshTokenAuthenticationStrategy((RefreshTokenAuthenticationSettings) settings);
+ } else {
+ val rootCauseException = new IllegalArgumentException(Messages.UNKNOWN_SETTINGS_TYPE);
+ throw new DataCloudJDBCException(Messages.UNKNOWN_SETTINGS_TYPE, "28000", rootCauseException);
+ }
+ }
+
+ @UtilityClass
+ class Messages {
+ static final String UNKNOWN_SETTINGS_TYPE = "Resolved settings were an unknown type of AuthenticationSettings";
+ }
+
+ @UtilityClass
+ class Keys {
+ static final String GRANT_TYPE = "grant_type";
+ static final String CLIENT_ID = "client_id";
+ static final String CLIENT_SECRET = "client_secret";
+ static final String USER_AGENT = "User-Agent";
+ }
+
+ FormCommand buildAuthenticate() throws SQLException;
+
+ AuthenticationSettings getSettings();
+}
+
+abstract class SharedAuthenticationStrategy implements AuthenticationStrategy {
+ protected final FormCommand.Builder builder(HttpCommandPath path) throws SQLException {
+ val settings = getSettings();
+ val builder = FormCommand.builder();
+
+ builder.url(settings.getLoginUri());
+ builder.suffix(path.getSuffix());
+
+ builder.header(Keys.USER_AGENT, settings.getUserAgent());
+
+ return builder;
+ }
+}
+
+@Getter
+@RequiredArgsConstructor
+class PasswordAuthenticationStrategy extends SharedAuthenticationStrategy {
+ private static final String GRANT_TYPE = "password";
+ private static final String USERNAME = "username";
+ private static final String PASSWORD = "password";
+
+ private final PasswordAuthenticationSettings settings;
+
+ /**
+ * username
+ * password flow docs
+ */
+ @Override
+ public FormCommand buildAuthenticate() throws SQLException {
+ val builder = super.builder(HttpCommandPath.AUTHENTICATE);
+
+ builder.bodyEntry(Keys.GRANT_TYPE, GRANT_TYPE);
+ builder.bodyEntry(USERNAME, settings.getUserName());
+ builder.bodyEntry(PASSWORD, settings.getPassword());
+ builder.bodyEntry(Keys.CLIENT_ID, settings.getClientId());
+ builder.bodyEntry(Keys.CLIENT_SECRET, settings.getClientSecret());
+
+ return builder.build();
+ }
+}
+
+@Getter
+@RequiredArgsConstructor
+class RefreshTokenAuthenticationStrategy extends SharedAuthenticationStrategy {
+ private static final String GRANT_TYPE = "refresh_token";
+ private static final String REFRESH_TOKEN = "refresh_token";
+
+ private final RefreshTokenAuthenticationSettings settings;
+
+ /**
+ * refresh
+ * token flow docs
+ */
+ @Override
+ public FormCommand buildAuthenticate() throws SQLException {
+ val builder = super.builder(HttpCommandPath.AUTHENTICATE);
+
+ builder.bodyEntry(Keys.GRANT_TYPE, GRANT_TYPE);
+ builder.bodyEntry(REFRESH_TOKEN, settings.getRefreshToken());
+ builder.bodyEntry(Keys.CLIENT_ID, settings.getClientId());
+ builder.bodyEntry(Keys.CLIENT_SECRET, settings.getClientSecret());
+
+ return builder.build();
+ }
+}
+
+@Getter
+@RequiredArgsConstructor
+class PrivateKeyAuthenticationStrategy extends SharedAuthenticationStrategy {
+ private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
+ private static final String ASSERTION = "assertion";
+
+ private final PrivateKeyAuthenticationSettings settings;
+
+ /**
+ * private key flow
+ * docs
+ */
+ @Override
+ public FormCommand buildAuthenticate() throws SQLException {
+ val builder = super.builder(HttpCommandPath.AUTHENTICATE);
+
+ builder.bodyEntry(Keys.GRANT_TYPE, GRANT_TYPE);
+ builder.bodyEntry(ASSERTION, JwtParts.buildJwt(settings));
+
+ return builder.build();
+ }
+}
+
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+class ExchangeTokenAuthenticationStrategy {
+ private static final String GRANT_TYPE = "urn:salesforce:grant-type:external:cdp";
+ private static final String ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
+ static final String SUBJECT_TOKEN_TYPE = "subject_token_type";
+ static final String SUBJECT_TOKEN_KEY = "subject_token";
+ static final String DATASPACE = "dataspace";
+
+ static ExchangeTokenAuthenticationStrategy of(@NonNull AuthenticationSettings settings, @NonNull OAuthToken token) {
+ return new ExchangeTokenAuthenticationStrategy(settings, token);
+ }
+
+ @Getter
+ private final AuthenticationSettings settings;
+
+ private final OAuthToken token;
+
+ /**
+ * exchange
+ * token flow docs
+ */
+ public FormCommand toCommand() {
+ val builder = FormCommand.builder();
+
+ builder.url(token.getInstanceUrl());
+ builder.suffix(HttpCommandPath.EXCHANGE.getSuffix());
+
+ builder.header(AuthenticationStrategy.Keys.USER_AGENT, settings.getUserAgent());
+
+ builder.bodyEntry(AuthenticationStrategy.Keys.GRANT_TYPE, GRANT_TYPE);
+ builder.bodyEntry(SUBJECT_TOKEN_TYPE, ACCESS_TOKEN);
+ builder.bodyEntry(SUBJECT_TOKEN_KEY, token.getToken());
+
+ if (StringUtils.isNotBlank(settings.getDataspace())) {
+ builder.bodyEntry(DATASPACE, settings.getDataspace());
+ }
+
+ return builder.build();
+ }
+}
+
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+class RevokeTokenAuthenticationStrategy {
+ static final String REVOKE_TOKEN_KEY = "token";
+
+ static RevokeTokenAuthenticationStrategy of(@NonNull AuthenticationSettings settings, @NonNull OAuthToken token) {
+ return new RevokeTokenAuthenticationStrategy(settings, token);
+ }
+
+ @Getter
+ private final AuthenticationSettings settings;
+
+ private final OAuthToken token;
+
+ public FormCommand toCommand() {
+ val builder = FormCommand.builder();
+
+ builder.url(token.getInstanceUrl());
+ builder.suffix(HttpCommandPath.REVOKE.getSuffix());
+
+ builder.header(AuthenticationStrategy.Keys.USER_AGENT, settings.getUserAgent());
+
+ builder.bodyEntry(REVOKE_TOKEN_KEY, token.getToken());
+
+ return builder.build();
+ }
+}
+
+@Getter
+enum HttpCommandPath {
+ AUTHENTICATE("services/oauth2/token"),
+ EXCHANGE("services/a360/token"),
+ REVOKE("services/oauth2/revoke");
+
+ private final URI suffix;
+
+ @SneakyThrows
+ HttpCommandPath(String suffix) {
+ this.suffix = new URI(suffix);
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/DataCloudToken.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/DataCloudToken.java
new file mode 100644
index 0000000..f4729f9
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/DataCloudToken.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import static com.salesforce.datacloud.jdbc.util.Require.requireNotNullOrBlank;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.salesforce.datacloud.jdbc.auth.model.DataCloudTokenResponse;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.util.Messages;
+import java.io.IOException;
+import java.net.URI;
+import java.sql.SQLException;
+import java.util.Base64;
+import java.util.Calendar;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.commons.lang3.StringUtils;
+
+@Slf4j
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public class DataCloudToken {
+ private static final int JWT_PAYLOAD_INDEX = 1;
+ private static final String JWT_DELIMITER = "\\.";
+ private static final String AUDIENCE_TENANT_ID = "audienceTenantId";
+
+ private final String type;
+ private final String token;
+ private final URI tenant;
+ private final Calendar expiresIn;
+
+ private static final String TENANT_IO_ERROR_RESPONSE = "Error while decoding tenantId.";
+
+ public static DataCloudToken of(DataCloudTokenResponse model) throws SQLException {
+ val type = model.getTokenType();
+ val token = model.getToken();
+ val tenantUrl = model.getInstanceUrl();
+
+ requireNotNullOrBlank(type, "token_type");
+ requireNotNullOrBlank(token, "access_token");
+ requireNotNullOrBlank(tenantUrl, "instance_url");
+
+ val expiresIn = Calendar.getInstance();
+ expiresIn.add(Calendar.SECOND, model.getExpiresIn());
+
+ try {
+ val tenant = URI.create(tenantUrl);
+
+ return new DataCloudToken(type, token, tenant, expiresIn);
+ } catch (IllegalArgumentException ex) {
+ val rootCauseException = new IllegalArgumentException(
+ "Failed to parse the provided tenantUrl: '" + tenantUrl + "'. " + ex.getMessage(), ex.getCause());
+ throw new DataCloudJDBCException(Messages.FAILED_LOGIN, "28000", rootCauseException);
+ }
+ }
+
+ public boolean isAlive() {
+ val now = Calendar.getInstance();
+ return now.compareTo(expiresIn) <= 0;
+ }
+
+ public String getTenantUrl() {
+ return this.tenant.toString();
+ }
+
+ public String getTenantId() throws SQLException {
+ return getTenantId(this.token);
+ }
+
+ public String getAccessToken() {
+ return this.type + StringUtils.SPACE + this.token;
+ }
+
+ private static String getTenantId(String token) throws SQLException {
+ String[] chunks = token.split(JWT_DELIMITER, -1);
+ Base64.Decoder decoder = Base64.getUrlDecoder();
+ try {
+ val chunk = chunks[JWT_PAYLOAD_INDEX];
+ val decodedChunk = decoder.decode(chunk);
+
+ return new ObjectMapper()
+ .readTree(decodedChunk)
+ .get(AUDIENCE_TENANT_ID)
+ .asText();
+ } catch (IOException e) {
+ throw new DataCloudJDBCException(TENANT_IO_ERROR_RESPONSE, "58030", e);
+ }
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/DataCloudTokenProcessor.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/DataCloudTokenProcessor.java
new file mode 100644
index 0000000..a07bcbf
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/DataCloudTokenProcessor.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import static com.salesforce.datacloud.jdbc.util.PropertiesExtensions.getIntegerOrDefault;
+import static org.apache.commons.lang3.StringUtils.isBlank;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+import com.salesforce.datacloud.jdbc.auth.errors.AuthorizationException;
+import com.salesforce.datacloud.jdbc.auth.model.AuthenticationResponseWithError;
+import com.salesforce.datacloud.jdbc.auth.model.DataCloudTokenResponse;
+import com.salesforce.datacloud.jdbc.auth.model.OAuthTokenResponse;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.http.ClientBuilder;
+import com.salesforce.datacloud.jdbc.http.FormCommand;
+import java.sql.SQLException;
+import java.time.temporal.ChronoUnit;
+import java.util.Properties;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import net.jodah.failsafe.Failsafe;
+import net.jodah.failsafe.FailsafeException;
+import net.jodah.failsafe.RetryPolicy;
+import net.jodah.failsafe.function.CheckedSupplier;
+import okhttp3.OkHttpClient;
+
+@Slf4j
+@Builder(access = AccessLevel.PRIVATE)
+public class DataCloudTokenProcessor implements TokenProcessor {
+ static final String MAX_RETRIES_KEY = "maxRetries";
+ static final int DEFAULT_MAX_RETRIES = 3;
+
+ private static final String CORE_ERROR_RESPONSE = "Received an error when acquiring oauth access token";
+
+ private static final String OFF_CORE_ERROR_RESPONSE =
+ "Received an error when exchanging oauth access token for data cloud token";
+
+ @Getter
+ private AuthenticationSettings settings;
+
+ private AuthenticationStrategy strategy;
+ private OkHttpClient client;
+ private TokenCache cache;
+ private RetryPolicy policy;
+ private RetryPolicy exponentialBackOffPolicy;
+
+ private AuthenticationResponseWithError getTokenWithRetry(CheckedSupplier response)
+ throws SQLException {
+ try {
+ return Failsafe.with(this.policy).get(response);
+ } catch (FailsafeException ex) {
+ if (ex.getCause() != null) {
+ throw new DataCloudJDBCException(ex.getCause().getMessage(), "28000", ex);
+ }
+ throw new DataCloudJDBCException(ex.getMessage(), "28000", ex);
+ }
+ }
+
+ private AuthenticationResponseWithError getDataCloudTokenWithRetry(
+ CheckedSupplier response) throws SQLException {
+ try {
+ return Failsafe.with(this.exponentialBackOffPolicy).get(response);
+ } catch (FailsafeException ex) {
+ if (ex.getCause() != null) {
+ throw new DataCloudJDBCException(ex.getCause().getMessage(), "28000", ex);
+ }
+ throw new DataCloudJDBCException(ex.getMessage(), "28000", ex);
+ }
+ }
+
+ private OAuthToken fetchOAuthToken() throws SQLException {
+ val command = strategy.buildAuthenticate();
+ val model = (OAuthTokenResponse) getTokenWithRetry(() -> {
+ val response = FormCommand.post(this.client, command, OAuthTokenResponse.class);
+ return throwExceptionOnError(response, CORE_ERROR_RESPONSE);
+ });
+ return OAuthToken.of(model);
+ }
+
+ private DataCloudToken fetchDataCloudToken() throws SQLException {
+ val model = (DataCloudTokenResponse) getDataCloudTokenWithRetry(() -> {
+ val oauthToken = getOAuthToken();
+ val command =
+ ExchangeTokenAuthenticationStrategy.of(settings, oauthToken).toCommand();
+ val response = FormCommand.post(this.client, command, DataCloudTokenResponse.class);
+ return throwExceptionOnError(response, OFF_CORE_ERROR_RESPONSE);
+ });
+ return DataCloudToken.of(model);
+ }
+
+ @Override
+ public OAuthToken getOAuthToken() throws SQLException {
+ try {
+ return fetchOAuthToken();
+ } catch (Exception ex) {
+ throw new DataCloudJDBCException(ex.getMessage(), "28000", ex);
+ }
+ }
+
+ @Override
+ public DataCloudToken getDataCloudToken() throws SQLException {
+ val cachedDataCloudToken = cache.getDataCloudToken();
+ if (cachedDataCloudToken != null && cachedDataCloudToken.isAlive()) {
+ return cachedDataCloudToken;
+ }
+
+ try {
+ return retrieveAndCacheDataCloudToken();
+ } catch (Exception ex) {
+ throw new DataCloudJDBCException(ex.getMessage(), "28000", ex);
+ }
+ }
+
+ private DataCloudToken retrieveAndCacheDataCloudToken() throws SQLException {
+ try {
+ val dataCloudToken = fetchDataCloudToken();
+ cache.setDataCloudToken(dataCloudToken);
+ return dataCloudToken;
+ } catch (Exception ex) {
+ cache.clearDataCloudToken();
+ throw new DataCloudJDBCException(ex.getMessage(), "28000", ex);
+ }
+ }
+
+ private static AuthenticationResponseWithError throwExceptionOnError(
+ AuthenticationResponseWithError response, String message) throws SQLException {
+ val token = response.getToken();
+ val code = response.getErrorCode();
+ val description = response.getErrorDescription();
+
+ if (isNotBlank(token) && isNotBlank(code) && isNotBlank(description)) {
+ log.warn("{} but got error code {} : {}", message, code, description);
+ } else if (isNotBlank(code) || isNotBlank(description)) {
+ val authorizationException = AuthorizationException.builder()
+ .message(message + ". " + code + ": " + description)
+ .errorCode(code)
+ .errorDescription(description)
+ .build();
+ throw new DataCloudJDBCException(authorizationException.getMessage(), "28000", authorizationException);
+ } else if (isBlank(token)) {
+ throw new DataCloudJDBCException(message + ", no token in response.", "28000");
+ }
+
+ return response;
+ }
+
+ public static DataCloudTokenProcessor of(Properties properties) throws SQLException {
+ val settings = AuthenticationSettings.of(properties);
+ val strategy = AuthenticationStrategy.of(settings);
+ val client = ClientBuilder.buildOkHttpClient(properties);
+ val policy = buildRetryPolicy(properties);
+ val exponentialBackOffPolicy = buildExponentialBackoffRetryPolicy(properties);
+ val cache = new TokenCacheImpl();
+
+ return DataCloudTokenProcessor.builder()
+ .client(client)
+ .policy(policy)
+ .exponentialBackOffPolicy(exponentialBackOffPolicy)
+ .cache(cache)
+ .strategy(strategy)
+ .settings(settings)
+ .build();
+ }
+
+ static RetryPolicy buildRetryPolicy(Properties properties) {
+ val maxRetries = getIntegerOrDefault(properties, MAX_RETRIES_KEY, DEFAULT_MAX_RETRIES);
+ return new RetryPolicy()
+ .withMaxRetries(maxRetries)
+ .handleIf(e -> !(e instanceof AuthorizationException));
+ }
+
+ static RetryPolicy buildExponentialBackoffRetryPolicy(Properties properties) {
+ val maxRetries = getIntegerOrDefault(properties, MAX_RETRIES_KEY, DEFAULT_MAX_RETRIES);
+ return new RetryPolicy()
+ .withMaxRetries(maxRetries)
+ .withBackoff(1, 30, ChronoUnit.SECONDS)
+ .handleIf(e -> !(e instanceof AuthorizationException));
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/OAuthToken.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/OAuthToken.java
new file mode 100644
index 0000000..ed98e09
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/OAuthToken.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import com.salesforce.datacloud.jdbc.auth.model.OAuthTokenResponse;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.util.Messages;
+import java.net.URI;
+import java.sql.SQLException;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Value;
+import lombok.val;
+import org.apache.commons.lang3.StringUtils;
+
+@Value
+@Builder(access = AccessLevel.PRIVATE)
+public class OAuthToken {
+ private static final String BEARER_PREFIX = "Bearer ";
+
+ String token;
+ URI instanceUrl;
+
+ public static OAuthToken of(OAuthTokenResponse response) throws SQLException {
+ val accessToken = response.getToken();
+
+ if (StringUtils.isBlank(accessToken)) {
+ throw new DataCloudJDBCException(Messages.FAILED_LOGIN, "28000");
+ }
+
+ try {
+ val instanceUrl = new URI(response.getInstanceUrl());
+
+ return OAuthToken.builder()
+ .token(accessToken)
+ .instanceUrl(instanceUrl)
+ .build();
+ } catch (Exception ex) {
+ throw new DataCloudJDBCException(Messages.FAILED_LOGIN, "28000", ex);
+ }
+ }
+
+ public String getBearerToken() {
+ return BEARER_PREFIX + getToken();
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/PrivateKeyHelpers.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/PrivateKeyHelpers.java
new file mode 100644
index 0000000..74ab319
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/PrivateKeyHelpers.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import java.security.KeyFactory;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.sql.SQLException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Date;
+import lombok.Getter;
+import lombok.experimental.UtilityClass;
+import lombok.val;
+
+@Getter
+enum Audience {
+ DEV("login.test1.pc-rnd.salesforce.com"),
+ PROD("login.salesforce.com");
+
+ public final String url;
+
+ Audience(String audience) {
+ this.url = audience;
+ }
+
+ public static Audience of(String url) throws SQLException {
+ if (url.contains(TEST_SUFFIX)) {
+ return Audience.DEV;
+ } else if (url.endsWith(PROD_SUFFIX)) {
+ return Audience.PROD;
+ } else {
+ val errorMessage = "The specified url: '" + url + "' didn't match any known environments";
+ val rootCauseException = new IllegalArgumentException(errorMessage);
+ throw new DataCloudJDBCException(errorMessage, "28000", rootCauseException);
+ }
+ }
+
+ private static final String PROD_SUFFIX = ".salesforce.com";
+ private static final String TEST_SUFFIX = ".test1.pc-rnd.salesforce.com";
+}
+
+@UtilityClass
+class JwtParts {
+ public static String buildJwt(PrivateKeyAuthenticationSettings settings) throws SQLException {
+ try {
+ Instant now = Instant.now();
+ Audience audience = Audience.of(settings.getLoginUrl());
+ RSAPrivateKey privateKey = asPrivateKey(settings.getPrivateKey());
+ return Jwts.builder()
+ .setIssuer(settings.getClientId())
+ .setSubject(settings.getUserName())
+ .setAudience(audience.url)
+ .setIssuedAt(Date.from(now))
+ .setExpiration(Date.from(now.plus(2L, ChronoUnit.MINUTES)))
+ .signWith(privateKey, SignatureAlgorithm.RS256)
+ .compact();
+ } catch (Exception ex) {
+ throw new DataCloudJDBCException(JWT_CREATION_FAILURE, "28000", ex);
+ }
+ }
+
+ private static RSAPrivateKey asPrivateKey(String privateKey) throws SQLException {
+ String rsaPrivateKey = privateKey
+ .replaceFirst(BEGIN_PRIVATE_KEY, "")
+ .replaceFirst(END_PRIVATE_KEY, "")
+ .replaceAll("\\s", "");
+
+ val bytes = decodeBase64(rsaPrivateKey);
+
+ try {
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
+ val factory = KeyFactory.getInstance("RSA");
+ return (RSAPrivateKey) factory.generatePrivate(keySpec);
+
+ } catch (Exception ex) {
+ throw new DataCloudJDBCException(JWT_CREATION_FAILURE, "28000", ex);
+ }
+ }
+
+ private byte[] decodeBase64(String input) {
+ return Base64.getDecoder().decode(input);
+ }
+
+ private static final String JWT_CREATION_FAILURE =
+ "JWT assertion creation failed. Please check Username, Client Id, Private key and try again.";
+ private static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
+ private static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/TokenCache.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/TokenCache.java
new file mode 100644
index 0000000..068b294
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/TokenCache.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+interface TokenCache {
+ void setDataCloudToken(DataCloudToken dataCloudToken);
+
+ void clearDataCloudToken();
+
+ DataCloudToken getDataCloudToken();
+}
+
+class TokenCacheImpl implements TokenCache {
+ private DataCloudToken dataCloudToken;
+
+ @Override
+ public void setDataCloudToken(DataCloudToken dataCloudToken) {
+ this.dataCloudToken = dataCloudToken;
+ }
+
+ @Override
+ public void clearDataCloudToken() {
+ this.dataCloudToken = null;
+ }
+
+ @Override
+ public DataCloudToken getDataCloudToken() {
+ return this.dataCloudToken;
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/TokenProcessor.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/TokenProcessor.java
new file mode 100644
index 0000000..19ea97d
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/TokenProcessor.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth;
+
+import java.sql.SQLException;
+
+public interface TokenProcessor {
+ AuthenticationSettings getSettings();
+
+ OAuthToken getOAuthToken() throws SQLException;
+
+ DataCloudToken getDataCloudToken() throws SQLException;
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/errors/AuthorizationException.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/errors/AuthorizationException.java
new file mode 100644
index 0000000..9109939
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/errors/AuthorizationException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth.errors;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class AuthorizationException extends Exception {
+ private final String message;
+ private final String errorCode;
+ private final String errorDescription;
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/model/AuthenticationResponseWithError.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/model/AuthenticationResponseWithError.java
new file mode 100644
index 0000000..cf9c927
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/model/AuthenticationResponseWithError.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth.model;
+
+/**
+ * * Check out the error code docs
+ */
+public interface AuthenticationResponseWithError {
+ String getToken();
+
+ String getErrorCode();
+
+ String getErrorDescription();
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/model/DataCloudTokenResponse.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/model/DataCloudTokenResponse.java
new file mode 100644
index 0000000..e43e4d0
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/model/DataCloudTokenResponse.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * The shape of this response can be found here
+ * under the heading "Exchanging Access Token for Data Cloud Token"
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class DataCloudTokenResponse implements AuthenticationResponseWithError {
+ @JsonProperty("access_token")
+ private String token;
+
+ @JsonProperty("instance_url")
+ private String instanceUrl;
+
+ @JsonProperty("token_type")
+ private String tokenType;
+
+ @JsonProperty("expires_in")
+ private int expiresIn;
+
+ @JsonProperty("error")
+ private String errorCode;
+
+ @JsonProperty("error_description")
+ private String errorDescription;
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/auth/model/OAuthTokenResponse.java b/src/main/java/com/salesforce/datacloud/jdbc/auth/model/OAuthTokenResponse.java
new file mode 100644
index 0000000..3f18c13
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/auth/model/OAuthTokenResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.auth.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * The shape of this response can be found here under the
+ * heading "Salesforce Grants a New Access Token"
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class OAuthTokenResponse implements AuthenticationResponseWithError {
+ private String scope;
+
+ @JsonProperty("access_token")
+ private String token;
+
+ @JsonProperty("instance_url")
+ private String instanceUrl;
+
+ @JsonProperty("token_type")
+ private String tokenType;
+
+ @JsonProperty("issued_at")
+ private String issuedAt;
+
+ @JsonProperty("error")
+ private String errorCode;
+
+ @JsonProperty("error_description")
+ private String errorDescription;
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/config/DriverVersion.java b/src/main/java/com/salesforce/datacloud/jdbc/config/DriverVersion.java
new file mode 100644
index 0000000..0a93c95
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/config/DriverVersion.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.config;
+
+import java.util.Properties;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Value;
+import lombok.experimental.UtilityClass;
+import lombok.val;
+
+@UtilityClass
+public class DriverVersion {
+ private static final String DRIVER_NAME = "salesforce-datacloud-jdbc";
+ private static final String DATABASE_PRODUCT_NAME = "salesforce-datacloud-queryservice";
+ private static final String DATABASE_PRODUCT_VERSION = "1.0";
+
+ @Getter(lazy = true)
+ private static final DriverVersionInfo driverVersionInfo = DriverVersionInfo.of();
+
+ public static int getMajorVersion() {
+ return getDriverVersionInfo().getMajor();
+ }
+
+ public static int getMinorVersion() {
+ return getDriverVersionInfo().getMinor();
+ }
+
+ public static String getDriverName() {
+ return DRIVER_NAME;
+ }
+
+ public static String getProductName() {
+ return DATABASE_PRODUCT_NAME;
+ }
+
+ public static String getProductVersion() {
+ return DATABASE_PRODUCT_VERSION;
+ }
+
+ public static String formatDriverInfo() {
+ return String.format("%s/%s", getDriverName(), getDriverVersionInfo());
+ }
+
+ public static String getDriverVersion() {
+ return getDriverVersionInfo().toString();
+ }
+}
+
+@Value
+@Builder(access = AccessLevel.PRIVATE)
+class DriverVersionInfo {
+ @Builder.Default
+ int major = 0;
+
+ @Builder.Default
+ int minor = 0;
+
+ static String getVersion(Properties properties) {
+ String version = properties.getProperty("version");
+ if (version == null || version.isEmpty()) {
+ return "0.0";
+ }
+ return version.replaceAll("-.*$", "");
+ }
+
+ static DriverVersionInfo of(Properties properties) {
+ val builder = DriverVersionInfo.builder();
+ String version = getVersion(properties);
+ if (!version.isEmpty()) {
+ val chunks = version.split("\\.", -1);
+ builder.major(Integer.parseInt(chunks[0]));
+ builder.minor(Integer.parseInt(chunks[1]));
+ return builder.build();
+ }
+ return builder.build();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%d.%d", major, minor);
+ }
+
+ static DriverVersionInfo of() {
+ val properties = ResourceReader.readResourceAsProperties("/version.properties");
+ return of(properties);
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/config/KeywordResources.java b/src/main/java/com/salesforce/datacloud/jdbc/config/KeywordResources.java
new file mode 100644
index 0000000..2218c3c
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/config/KeywordResources.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.config;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import lombok.experimental.UtilityClass;
+import lombok.val;
+
+@UtilityClass
+public class KeywordResources {
+
+ // spotless:off
+ public static final Set SQL_2003_KEYWORDS = Set.of("ADD","ALL","ALLOCATE","ALTER","AND","ANY","ARE","ARRAY",
+ "AS","ASENSITIVE","ASYMMETRIC","AT","ATOMIC","AUTHORIZATION","BEGIN","BETWEEN","BIGINT","BINARY","BLOB",
+ "BOOLEAN","BOTH","BY","CALL","CALLED","CASCADED","CASE","CAST","CHAR","CHARACTER","CHECK","CLOB","CLOSE",
+ "COLLATE","COLUMN","COMMIT","CONDITION","CONNECT","CONSTRAINT","CONTINUE","CORRESPONDING","CREATE","CROSS",
+ "CUBE","CURRENT","CURRENT_DATE","CURRENT_DEFAULT_TRANSFORM_GROUP","CURRENT_PATH","CURRENT_ROLE",
+ "CURRENT_TIME","CURRENT_TIMESTAMP","CURRENT_TRANSFORM_GROUP_FOR_TYPE","CURRENT_USER","CURSOR","CYCLE",
+ "DATE","DAY","DEALLOCATE","DEC","DECIMAL","DECLARE","DEFAULT","DELETE","DEREF","DESCRIBE","DETERMINISTIC",
+ "DISCONNECT","DISTINCT","DO","DOUBLE","DROP","DYNAMIC","EACH","ELEMENT","ELSE","ELSEIF","END","ESCAPE",
+ "EXCEPT","EXEC","EXECUTE","EXISTS","EXIT","EXTERNAL","FALSE","FETCH","FILTER","FLOAT","FOR","FOREIGN",
+ "FREE","FROM","FULL","FUNCTION","GET","GLOBAL","GRANT","GROUP","GROUPING","HANDLER","HAVING","HOLD","HOUR",
+ "IDENTITY","IF","IMMEDIATE","IN","INDICATOR","INNER","INOUT","INPUT","INSENSITIVE","INSERT","INT","INTEGER",
+ "INTERSECT","INTERVAL","INTO","IS","ITERATE","JOIN","LANGUAGE","LARGE","LATERAL","LEADING","LEAVE","LEFT",
+ "LIKE","LOCAL","LOCALTIME","LOCALTIMESTAMP","LOOP","MATCH","MEMBER","MERGE","METHOD","MINUTE","MODIFIES",
+ "MODULE","MONTH","MULTISET","NATIONAL","NATURAL","NCHAR","NCLOB","NEW","NO","NONE","NOT","NULL","NUMERIC",
+ "OF","OLD","ON","ONLY","OPEN","OR","ORDER","OUT","OUTER","OUTPUT","OVER","OVERLAPS","PARAMETER","PARTITION",
+ "PRECISION","PREPARE","PROCEDURE","RANGE","READS","REAL","RECURSIVE","REF","REFERENCES","REFERENCING",
+ "RELEASE","REPEAT","RESIGNAL","RESULT","RETURN","RETURNS","REVOKE","RIGHT","ROLLBACK","ROLLUP","ROW","ROWS",
+ "SAVEPOINT","SCOPE","SCROLL","SEARCH","SECOND","SELECT","SENSITIVE","SESSION_USER","SET","SIGNAL","SIMILAR",
+ "SMALLINT","SOME","SPECIFIC","SPECIFICTYPE","SQL","SQLEXCEPTION","SQLSTATE","SQLWARNING","START","STATIC",
+ "SUBMULTISET","SYMMETRIC","SYSTEM","SYSTEM_USER","TABLE","TABLESAMPLE","THEN","TIME","TIMESTAMP",
+ "TIMEZONE_HOUR","TIMEZONE_MINUTE","TO","TRAILING","TRANSLATION","TREAT","TRIGGER","TRUE","UNDO","UNION",
+ "UNIQUE","UNKNOWN","UNNEST","UNTIL","UPDATE","USER","USING","VALUE","VALUES","VARCHAR","VARYING","WHEN",
+ "WHENEVER","WHERE","WHILE","WINDOW","WITH","WITHIN","WITHOUT","YEAR");
+ // spotless:on
+
+ @Getter(lazy = true)
+ private final String sqlKeywords = loadSqlKeywords();
+
+ private static String loadSqlKeywords() {
+ val hyperSqlLexerKeywords = ResourceReader.readResourceAsStringList("/keywords/hyper_sql_lexer_keywords.txt");
+ val difference = hyperSqlLexerKeywords.stream()
+ .map(String::toUpperCase)
+ .distinct()
+ .filter(keyword -> !SQL_2003_KEYWORDS.contains(keyword))
+ .sorted()
+ .collect(Collectors.toList());
+ return String.join(",", difference);
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/config/QueryResources.java b/src/main/java/com/salesforce/datacloud/jdbc/config/QueryResources.java
new file mode 100644
index 0000000..f9aea78
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/config/QueryResources.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.config;
+
+import lombok.Getter;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class QueryResources {
+ @Getter(lazy = true)
+ private final String columnsQuery = loadQuery("get_columns_query");
+
+ @Getter(lazy = true)
+ private final String schemasQuery = loadQuery("get_schemas_query");
+
+ @Getter(lazy = true)
+ private final String tablesQuery = loadQuery("get_tables_query");
+
+ private static String loadQuery(String name) {
+ return ResourceReader.readResourceAsString("/sql/" + name + ".sql");
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/config/ResourceReader.java b/src/main/java/com/salesforce/datacloud/jdbc/config/ResourceReader.java
new file mode 100644
index 0000000..0974a78
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/config/ResourceReader.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.config;
+
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.util.SqlErrorCodes;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import lombok.NonNull;
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+@Slf4j
+@UtilityClass
+public class ResourceReader {
+ public static String readResourceAsString(@NonNull String path) {
+ val result = new AtomicReference();
+ withResourceAsStream(path, in -> result.set(new String(in.readAllBytes(), StandardCharsets.UTF_8)));
+ return result.get();
+ }
+
+ @SneakyThrows
+ public static Properties readResourceAsProperties(@NonNull String path) {
+ val result = new Properties();
+ withResourceAsStream(path, result::load);
+ return result;
+ }
+
+ @SneakyThrows
+ static void withResourceAsStream(String path, @NonNull IOExceptionThrowingConsumer consumer) {
+ try (val in = ResourceReader.class.getResourceAsStream(path)) {
+ if (in == null) {
+ val message = String.format("%s. path=%s", NOT_FOUND_MESSAGE, path);
+ throw new DataCloudJDBCException(message, SqlErrorCodes.UNDEFINED_FILE);
+ }
+
+ consumer.accept(in);
+ } catch (IOException e) {
+ val message = String.format("%s. path=%s", IO_EXCEPTION_MESSAGE, path);
+ log.error(message, e);
+ throw new DataCloudJDBCException(message, SqlErrorCodes.UNDEFINED_FILE, e);
+ }
+ }
+
+ public static List readResourceAsStringList(String path) {
+ return Arrays.stream(readResourceAsString(path).split("\n"))
+ .map(String::trim)
+ .collect(Collectors.toList());
+ }
+
+ private static final String NOT_FOUND_MESSAGE = "Resource file not found";
+ private static final String IO_EXCEPTION_MESSAGE = "Error while loading resource file";
+
+ @FunctionalInterface
+ public interface IOExceptionThrowingConsumer {
+ void accept(T t) throws IOException;
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/core/ArrowStreamReaderCursor.java b/src/main/java/com/salesforce/datacloud/jdbc/core/ArrowStreamReaderCursor.java
new file mode 100644
index 0000000..576d877
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/core/ArrowStreamReaderCursor.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.core;
+
+import static com.salesforce.datacloud.jdbc.util.ThrowingFunction.rethrowFunction;
+
+import com.salesforce.datacloud.jdbc.core.accessor.QueryJDBCAccessorFactory;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowStreamReader;
+import org.apache.calcite.avatica.ColumnMetaData;
+import org.apache.calcite.avatica.util.AbstractCursor;
+import org.apache.calcite.avatica.util.ArrayImpl;
+
+@AllArgsConstructor
+@Slf4j
+class ArrowStreamReaderCursor extends AbstractCursor {
+
+ private static final int INIT_ROW_NUMBER = -1;
+
+ private final ArrowStreamReader reader;
+
+ private final AtomicInteger currentRow = new AtomicInteger(INIT_ROW_NUMBER);
+
+ private void wasNullConsumer(boolean wasNull) {
+ this.wasNull[0] = wasNull;
+ }
+
+ @SneakyThrows
+ private VectorSchemaRoot getSchemaRoot() {
+ return reader.getVectorSchemaRoot();
+ }
+
+ @Override
+ @SneakyThrows
+ public List createAccessors(
+ List types, Calendar localCalendar, ArrayImpl.Factory factory) {
+ return getSchemaRoot().getFieldVectors().stream()
+ .map(rethrowFunction(this::createAccessor))
+ .collect(Collectors.toList());
+ }
+
+ private Accessor createAccessor(FieldVector vector) throws SQLException {
+ return QueryJDBCAccessorFactory.createAccessor(vector, currentRow::get, this::wasNullConsumer);
+ }
+
+ private boolean loadNextBatch() throws SQLException {
+ try {
+ if (reader.loadNextBatch()) {
+ currentRow.set(0);
+ return true;
+ }
+ } catch (IOException e) {
+ throw new DataCloudJDBCException(e);
+ }
+ return false;
+ }
+
+ @SneakyThrows
+ @Override
+ public boolean next() {
+ val current = currentRow.incrementAndGet();
+ val total = getSchemaRoot().getRowCount();
+
+ try {
+ return current < total || loadNextBatch();
+ } catch (Exception e) {
+ throw new DataCloudJDBCException("Failed to load next batch", e);
+ }
+ }
+
+ @Override
+ protected Getter createGetter(int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @SneakyThrows
+ @Override
+ public void close() {
+ reader.close();
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/core/DataCloudConnection.java b/src/main/java/com/salesforce/datacloud/jdbc/core/DataCloudConnection.java
new file mode 100644
index 0000000..d08b553
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/core/DataCloudConnection.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.core;
+
+import static com.salesforce.datacloud.jdbc.util.Constants.CONNECTION_PROTOCOL;
+import static com.salesforce.datacloud.jdbc.util.Constants.LOGIN_URL;
+import static com.salesforce.datacloud.jdbc.util.Constants.USER;
+import static com.salesforce.datacloud.jdbc.util.Constants.USER_NAME;
+
+import com.salesforce.datacloud.jdbc.auth.AuthenticationSettings;
+import com.salesforce.datacloud.jdbc.auth.DataCloudTokenProcessor;
+import com.salesforce.datacloud.jdbc.auth.TokenProcessor;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.http.ClientBuilder;
+import com.salesforce.datacloud.jdbc.interceptor.AuthorizationHeaderInterceptor;
+import com.salesforce.datacloud.jdbc.interceptor.DataspaceHeaderInterceptor;
+import com.salesforce.datacloud.jdbc.interceptor.HyperDefaultsHeaderInterceptor;
+import com.salesforce.datacloud.jdbc.interceptor.TracingHeadersInterceptor;
+import com.salesforce.datacloud.jdbc.interceptor.UserAgentHeaderInterceptor;
+import com.salesforce.datacloud.jdbc.util.Messages;
+import io.grpc.ClientInterceptor;
+import io.grpc.ManagedChannelBuilder;
+import java.net.URI;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Savepoint;
+import java.sql.Statement;
+import java.sql.Struct;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.commons.lang3.StringUtils;
+
+@Slf4j
+@Getter
+@Builder(access = AccessLevel.PACKAGE)
+public class DataCloudConnection implements Connection, AutoCloseable {
+ private static final int DEFAULT_PORT = 443;
+
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+
+ private final TokenProcessor tokenProcessor;
+
+ @NonNull @Builder.Default
+ private final Properties properties = new Properties();
+
+ @Setter
+ @Builder.Default
+ private List interceptors = new ArrayList<>();
+
+ @NonNull private final HyperGrpcClientExecutor executor;
+
+ public static DataCloudConnection fromChannel(@NonNull ManagedChannelBuilder> builder, Properties properties)
+ throws SQLException {
+ val interceptors = getClientInterceptors(null, properties);
+ val executor = HyperGrpcClientExecutor.of(builder.intercept(interceptors), properties);
+
+ return DataCloudConnection.builder()
+ .executor(executor)
+ .properties(properties)
+ .build();
+ }
+
+ /** This flow is not supported by the JDBC Driver Manager, only use it if you know what you're doing. */
+ public static DataCloudConnection fromTokenSupplier(
+ AuthorizationHeaderInterceptor authInterceptor, @NonNull String host, int port, Properties properties)
+ throws SQLException {
+ val channel = ManagedChannelBuilder.forAddress(host, port);
+ return fromTokenSupplier(authInterceptor, channel, properties);
+ }
+
+ /** This flow is not supported by the JDBC Driver Manager, only use it if you know what you're doing. */
+ public static DataCloudConnection fromTokenSupplier(
+ AuthorizationHeaderInterceptor authInterceptor,
+ @NonNull ManagedChannelBuilder> builder,
+ Properties properties)
+ throws SQLException {
+ val interceptors = getClientInterceptors(authInterceptor, properties);
+ val executor = HyperGrpcClientExecutor.of(builder.intercept(interceptors), properties);
+
+ return DataCloudConnection.builder()
+ .executor(executor)
+ .properties(properties)
+ .build();
+ }
+
+ static List getClientInterceptors(
+ AuthorizationHeaderInterceptor authInterceptor, Properties properties) {
+ return Stream.of(
+ authInterceptor,
+ new HyperDefaultsHeaderInterceptor(),
+ TracingHeadersInterceptor.of(),
+ UserAgentHeaderInterceptor.of(properties),
+ DataspaceHeaderInterceptor.of(properties))
+ .filter(Objects::nonNull)
+ .peek(t -> log.info("Registering interceptor. interceptor={}", t))
+ .collect(Collectors.toList());
+ }
+
+ public static DataCloudConnection of(String url, Properties properties) throws SQLException {
+ var serviceRootUrl = getServiceRootUrl(url);
+ properties.put(LOGIN_URL, serviceRootUrl);
+ addClientUsernameIfRequired(properties);
+
+ if (!AuthenticationSettings.hasAny(properties)) {
+ throw new DataCloudJDBCException("No authentication settings provided");
+ }
+
+ val tokenProcessor = DataCloudTokenProcessor.of(properties);
+
+ val host = tokenProcessor.getDataCloudToken().getTenantUrl();
+
+ val builder = ManagedChannelBuilder.forAddress(host, DEFAULT_PORT);
+ val authInterceptor = AuthorizationHeaderInterceptor.of(tokenProcessor);
+
+ val interceptors = getClientInterceptors(authInterceptor, properties);
+ val executor = HyperGrpcClientExecutor.of(builder.intercept(interceptors), properties);
+
+ return DataCloudConnection.builder()
+ .tokenProcessor(tokenProcessor)
+ .executor(executor)
+ .properties(properties)
+ .build();
+ }
+
+ @Override
+ public Statement createStatement() {
+ return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql) {
+ return getQueryPreparedStatement(sql);
+ }
+
+ private DataCloudPreparedStatement getQueryPreparedStatement(String sql) {
+ return new DataCloudPreparedStatement(this, sql, new DefaultParameterManager());
+ }
+
+ @Override
+ public CallableStatement prepareCall(String sql) {
+ return null;
+ }
+
+ @Override
+ public String nativeSQL(String sql) {
+ return sql;
+ }
+
+ @Override
+ public void setAutoCommit(boolean autoCommit) {}
+
+ @Override
+ public boolean getAutoCommit() {
+ return false;
+ }
+
+ @Override
+ public void commit() {}
+
+ @Override
+ public void rollback() {}
+
+ @Override
+ public void close() {
+ try {
+ if (closed.compareAndSet(false, true)) {
+ executor.close();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed.get();
+ }
+
+ @Override
+ public DatabaseMetaData getMetaData() {
+ val client = ClientBuilder.buildOkHttpClient(properties);
+ val userName = this.properties.getProperty("userName");
+ val loginUrl = this.properties.getProperty("loginURL");
+ return new DataCloudDatabaseMetadata(
+ getQueryStatement(), Optional.ofNullable(tokenProcessor), client, loginUrl, userName);
+ }
+
+ private @NonNull DataCloudStatement getQueryStatement() {
+ return new DataCloudStatement(this);
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {}
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public void setCatalog(String catalog) {}
+
+ @Override
+ public String getCatalog() {
+ return "";
+ }
+
+ @Override
+ public void setTransactionIsolation(int level) {}
+
+ @Override
+ public int getTransactionIsolation() {
+ return Connection.TRANSACTION_NONE;
+ }
+
+ @Override
+ public SQLWarning getWarnings() {
+ return null;
+ }
+
+ @Override
+ public void clearWarnings() {}
+
+ @Override
+ public Statement createStatement(int resultSetType, int resultSetConcurrency) {
+ return getQueryStatement();
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) {
+ return getQueryPreparedStatement(sql);
+ }
+
+ @Override
+ public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) {
+ return null;
+ }
+
+ @Override
+ public Map> getTypeMap() {
+ return null;
+ }
+
+ @Override
+ public void setTypeMap(Map> map) {}
+
+ @Override
+ public void setHoldability(int holdability) {}
+
+ @Override
+ public int getHoldability() {
+ return 0;
+ }
+
+ @Override
+ public Savepoint setSavepoint() {
+ return null;
+ }
+
+ @Override
+ public Savepoint setSavepoint(String name) {
+ return null;
+ }
+
+ @Override
+ public void rollback(Savepoint savepoint) {}
+
+ @Override
+ public void releaseSavepoint(Savepoint savepoint) {}
+
+ @Override
+ public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) {
+ return null;
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(
+ String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) {
+ return getQueryPreparedStatement(sql);
+ }
+
+ @Override
+ public CallableStatement prepareCall(
+ String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) {
+ return null;
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) {
+ return null;
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int[] columnIndexes) {
+ return null;
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, String[] columnNames) {
+ return null;
+ }
+
+ @Override
+ public Clob createClob() {
+ return null;
+ }
+
+ @Override
+ public Blob createBlob() {
+ return null;
+ }
+
+ @Override
+ public NClob createNClob() {
+ return null;
+ }
+
+ @Override
+ public SQLXML createSQLXML() {
+ return null;
+ }
+
+ @Override
+ public boolean isValid(int timeout) throws SQLException {
+ if (timeout < 0) {
+ throw new DataCloudJDBCException(String.format("Invalid timeout value: %d", timeout));
+ }
+ return !isClosed();
+ }
+
+ @Override
+ public void setClientInfo(String name, String value) {}
+
+ @Override
+ public void setClientInfo(Properties properties) {}
+
+ @Override
+ public String getClientInfo(String name) {
+ return "";
+ }
+
+ @Override
+ public Properties getClientInfo() {
+ return properties;
+ }
+
+ @Override
+ public Array createArrayOf(String typeName, Object[] elements) {
+ return null;
+ }
+
+ @Override
+ public Struct createStruct(String typeName, Object[] attributes) {
+ return null;
+ }
+
+ @Override
+ public void setSchema(String schema) {}
+
+ @Override
+ public String getSchema() {
+ return "";
+ }
+
+ @Override
+ public void abort(Executor executor) {}
+
+ @Override
+ public void setNetworkTimeout(Executor executor, int milliseconds) {}
+
+ @Override
+ public int getNetworkTimeout() {
+ return 0;
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ if (!iface.isInstance(this)) {
+ throw new DataCloudJDBCException(this.getClass().getName() + " not unwrappable from " + iface.getName());
+ }
+ return (T) this;
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) {
+ return iface.isInstance(this);
+ }
+
+ public static boolean acceptsUrl(String url) {
+ return url != null && url.startsWith(CONNECTION_PROTOCOL) && urlDoesNotContainScheme(url);
+ }
+
+ private static boolean urlDoesNotContainScheme(String url) {
+ val suffix = url.substring(CONNECTION_PROTOCOL.length());
+ return !suffix.startsWith("http://") && !suffix.startsWith("https://");
+ }
+
+ /**
+ * Returns the extracted service url from given jdbc endpoint
+ *
+ * @param url of the form jdbc:salesforce-datacloud://login.salesforce.com
+ * @return service url
+ * @throws SQLException when given url doesn't belong with required datasource
+ */
+ static String getServiceRootUrl(String url) throws SQLException {
+ if (!acceptsUrl(url)) {
+ throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL);
+ }
+
+ val serviceRootUrl = url.substring(CONNECTION_PROTOCOL.length());
+ val noTrailingSlash = StringUtils.removeEnd(serviceRootUrl, "/");
+ val host = StringUtils.removeStart(noTrailingSlash, "//");
+
+ return host.isBlank() ? host : createURI(host).toString();
+ }
+
+ private static URI createURI(String host) throws SQLException {
+ try {
+ return URI.create("https://" + host);
+ } catch (IllegalArgumentException e) {
+ throw new DataCloudJDBCException(Messages.ILLEGAL_CONNECTION_PROTOCOL, e);
+ }
+ }
+
+ static void addClientUsernameIfRequired(Properties properties) {
+ if (properties.containsKey(USER)) {
+ properties.computeIfAbsent(USER_NAME, p -> properties.get(USER));
+ }
+ }
+}
diff --git a/src/main/java/com/salesforce/datacloud/jdbc/core/DataCloudDatabaseMetadata.java b/src/main/java/com/salesforce/datacloud/jdbc/core/DataCloudDatabaseMetadata.java
new file mode 100644
index 0000000..4d7cedc
--- /dev/null
+++ b/src/main/java/com/salesforce/datacloud/jdbc/core/DataCloudDatabaseMetadata.java
@@ -0,0 +1,967 @@
+/*
+ * Copyright (c) 2024, Salesforce, Inc.
+ *
+ * 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.salesforce.datacloud.jdbc.core;
+
+import com.salesforce.datacloud.jdbc.auth.TokenProcessor;
+import com.salesforce.datacloud.jdbc.config.KeywordResources;
+import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
+import com.salesforce.datacloud.jdbc.util.Constants;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.RowIdLifetime;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+
+@Slf4j
+public class DataCloudDatabaseMetadata implements DatabaseMetaData {
+ private final DataCloudStatement dataCloudStatement;
+ private final Optional tokenProcessor;
+ private final OkHttpClient client;
+ private final String loginURL;
+ private final String userName;
+
+ public DataCloudDatabaseMetadata(
+ DataCloudStatement dataCloudStatement,
+ Optional tokenProcessor,
+ OkHttpClient client,
+ String loginURL,
+ String userName) {
+ this.dataCloudStatement = dataCloudStatement;
+ this.tokenProcessor = tokenProcessor;
+ this.client = client;
+ this.loginURL = loginURL;
+ this.userName = userName;
+ }
+
+ @Override
+ public boolean allProceduresAreCallable() {
+ return false;
+ }
+
+ @Override
+ public boolean allTablesAreSelectable() {
+ return true;
+ }
+
+ @Override
+ public String getURL() {
+ return loginURL;
+ }
+
+ @Override
+ public String getUserName() {
+ return userName;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public boolean nullsAreSortedHigh() {
+ return false;
+ }
+
+ @Override
+ public boolean nullsAreSortedLow() {
+ return true;
+ }
+
+ @Override
+ public boolean nullsAreSortedAtStart() {
+ return false;
+ }
+
+ @Override
+ public boolean nullsAreSortedAtEnd() {
+ return false;
+ }
+
+ @Override
+ public String getDatabaseProductName() {
+ return Constants.DATABASE_PRODUCT_NAME;
+ }
+
+ @Override
+ public String getDatabaseProductVersion() {
+ return Constants.DATABASE_PRODUCT_VERSION;
+ }
+
+ @Override
+ public String getDriverName() {
+ return Constants.DRIVER_NAME;
+ }
+
+ @Override
+ public String getDriverVersion() {
+ return Constants.DRIVER_VERSION;
+ }
+
+ @Override
+ public int getDriverMajorVersion() {
+ return 1;
+ }
+
+ @Override
+ public int getDriverMinorVersion() {
+ return 0;
+ }
+
+ @Override
+ public boolean usesLocalFiles() {
+ return false;
+ }
+
+ @Override
+ public boolean usesLocalFilePerTable() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsMixedCaseIdentifiers() {
+ return false;
+ }
+
+ @Override
+ public boolean storesUpperCaseIdentifiers() {
+ return false;
+ }
+
+ @Override
+ public boolean storesLowerCaseIdentifiers() {
+ return true;
+ }
+
+ @Override
+ public boolean storesMixedCaseIdentifiers() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsMixedCaseQuotedIdentifiers() {
+ return true;
+ }
+
+ @Override
+ public boolean storesUpperCaseQuotedIdentifiers() {
+ return false;
+ }
+
+ @Override
+ public boolean storesLowerCaseQuotedIdentifiers() {
+ return false;
+ }
+
+ @Override
+ public boolean storesMixedCaseQuotedIdentifiers() {
+ return false;
+ }
+
+ @Override
+ public String getIdentifierQuoteString() {
+ return "\"";
+ }
+
+ @Override
+ public String getSQLKeywords() {
+ return KeywordResources.getSqlKeywords();
+ }
+
+ @Override
+ public String getNumericFunctions() {
+ return null;
+ }
+
+ @Override
+ public String getStringFunctions() {
+ return null;
+ }
+
+ @Override
+ public String getSystemFunctions() {
+ return null;
+ }
+
+ @Override
+ public String getTimeDateFunctions() {
+ return null;
+ }
+
+ @Override
+ public String getSearchStringEscape() {
+ return "\\";
+ }
+
+ @Override
+ public String getExtraNameCharacters() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsAlterTableWithAddColumn() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsAlterTableWithDropColumn() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsColumnAliasing() {
+ return true;
+ }
+
+ @Override
+ public boolean nullPlusNonNullIsNull() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsConvert() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsConvert(int fromType, int toType) {
+ return true;
+ }
+
+ @Override
+ public boolean supportsTableCorrelationNames() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsDifferentTableCorrelationNames() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsExpressionsInOrderBy() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsOrderByUnrelated() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsGroupBy() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsGroupByUnrelated() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsGroupByBeyondSelect() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsLikeEscapeClause() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsMultipleResultSets() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsMultipleTransactions() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsNonNullableColumns() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *