├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs └── birt │ ├── Data source - property binding.png │ └── Salesforce JDBC sample.rptdesign ├── pom.xml ├── sf-auth-client ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ascendix │ │ │ └── salesforce │ │ │ ├── oauth │ │ │ ├── BadOAuthTokenException.java │ │ │ ├── ForceClientException.java │ │ │ ├── ForceOAuthClient.java │ │ │ └── ForceUserInfo.java │ │ │ └── soap │ │ │ └── ForceSoapValidator.java │ └── resources │ │ └── forceSoapBody │ └── test │ └── java │ └── util │ └── DBTablePrinter.java └── sf-jdbc-driver ├── pom.xml └── src ├── main └── java │ └── com │ └── ascendix │ └── jdbc │ └── salesforce │ ├── ForceDriver.java │ ├── connection │ ├── ForceConnection.java │ ├── ForceConnectionInfo.java │ └── ForceService.java │ ├── delegates │ ├── ForceResultField.java │ ├── PartnerResultToCrtesianTable.java │ └── PartnerService.java │ ├── metadata │ ├── Column.java │ ├── ColumnMap.java │ ├── ForceDatabaseMetaData.java │ └── Table.java │ ├── resultset │ ├── CachedResultSet.java │ └── CachedResultSetMetaData.java │ └── statement │ ├── FieldDef.java │ ├── ForcePreparedStatement.java │ ├── ParameterMetadataImpl.java │ └── SoqlQueryAnalyzer.java └── test ├── java ├── com │ └── ascendix │ │ └── jdbc │ │ └── salesforce │ │ ├── ForceDriverTest.java │ │ ├── delegates │ │ └── PartnerResultToCrtesianTableTest.java │ │ ├── metadata │ │ └── ForceDatabaseMetaDataTest.java │ │ ├── resultset │ │ └── CachedResultSetTest.java │ │ └── statement │ │ ├── ForcePreparedStatementTest.java │ │ └── SoqlQueryAnalyzerTest.java └── util │ └── DBTablePrinter.java └── resources ├── Account_desription.xml └── Contact_desription.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .factorypath 4 | .DS_Store 5 | *.iml 6 | .classpath 7 | .project 8 | .settings 9 | *.log 10 | target 11 | .metadata 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | jdk: 4 | - oraclejdk8 5 | cache: 6 | directories: 7 | - ~/.m2/repository 8 | - ~/.sonar/cache 9 | addons: 10 | sonarcloud: 11 | organization: "ascendix" 12 | token: 13 | secure: "OujfBaq2GKABK3q1qVKvXdImbcyMGNOR86a0uch/oK9LfqhSxvoy9yENyltVYpQezeHfhzsky260RT8LyqNhmF/MkZFVpl7cp+CiqW/QSt7W8tH8NmSgt09pF0bqg8YNmM3/DWNKWaN8GbSXVdqliEOUO8ccVNahBNuD396oaig5jRFtV4tZRKx+DlScfqQfZtPZR/lndV5I7QRNHHFELDmWDQfiorO5070X9ppPaqUR1nclfTCVBSDA/MufIKbAMnM0i4vkacf9EiRX3g3sTqs8LxqED5+PGiwfqGgQRLDT3N9WCxOf+40ZrTS15kHISAq1grH4B6FmyBpLKCB/zmluLFcnyNWdrOsiqfqvkjdZwC1iXsfwjeQUfYB1RyqJ4DxoD4OAV/83NLwa5owZTbYkA6oZioJKHBvIC5vffrFqh80IC79vj1bkim0KkjVEJ1zH/nD/Ty5E8am2xSMnTyt4SUAwuybjqG55Gs0MCOjcuf57pDr42IKtwcPvI9YUPAhJovkYOiU8mQPKuFI5+E0JegAnw2t+Cfo7pCLJkhhnXwp8w8EhN6OYcEhzAKKOGs49Eg2mg9LBWxuiCKNAxfLCSVXRWV+b9A6F3oDqQIDCwa0zLBIpikY7RNQZzxzCptzXM10RbSHZokN+36lW5r4jrrzWEn1RxFdS9fWZEPA=" 14 | script: mvn -B clean org.jacoco:jacoco-maven-plugin:prepare-agent package sonar:sonar 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ascendix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sforce-jdbc [![Build Status](https://api.travis-ci.org/ascendix/salesforce-jdbc.svg?branch=master)](https://travis-ci.org/ascendix/salesforce-jdbc) [![Build Status](https://sonarcloud.io/api/project_badges/measure?project=com.ascendix.salesforce%3Asalesforce-jdbc&metric=alert_status)](https://sonarcloud.io/dashboard?id=com.ascendix.salesforce%3Asalesforce-jdbc) 2 | Salesforce JDBC driver allows Java programs to connect to Salesforce data services using standard, database independent Java code. It is an open source JDBC driver written in Pure Java, and communicates over SOAP/HTTP(S) protocol. 3 | 4 | The main purpose of the driver is to retrieve (only) data from Salesforce services for data analysis. Primary target platform for the driver usage is Eclipse BIRT engine. 5 | 6 | ## Supported Salesforce and Java versions 7 | The current version of the driver should be compatible with **Salesforce Partner API version 39.0 and higher** and **Java 8**. 8 | 9 | ## Get the driver 10 | Download the driver [here](https://github.com/ascendix/mvnrepo/raw/master/com/ascendix/salesforce/salesforce-jdbc/1.1-SNAPSHOT/salesforce-jdbc-1.1-20180403.104727-1-single.jar) 11 | 12 | 13 | ## With Maven 14 | 15 | ### Add repositories 16 | 17 | 18 | com.ascendix.maven 19 | Ascendix Maven Repo 20 | https://github.com/ascendix/mvnrepo/raw/master 21 | 22 | 23 | mulesoft-releases 24 | MuleSoft Releases Repository 25 | http://repository.mulesoft.org/releases/ 26 | default 27 | 28 | 29 | 30 | ### Add dependency 31 | 32 | com.ascendix.salesforce 33 | salesforce-jdbc 34 | 1.1-20180403.104727-1 35 | 36 | 37 | 38 | ## How to connect 39 | 40 | ### Driver class name 41 | com.ascendix.jdbc.salesforce.ForceDriver 42 | 43 | ### JDBC URL format 44 | ``` 45 | jdbc:ascendix:salesforce://[;propertyName1=propertyValue1[;propertyName2=propertyValue2]...] 46 | ``` 47 | There are two ways to connect to Salesforce: 48 | 1. by using _user_ and _password_; 49 | 2. by using _sessionId_. 50 | 51 | _User_ and _password_ parameters are ignored if _sessionId_ parameter is set. 52 | 53 | An example for a connection URL with _user_ and _password_ parameters: 54 | ``` 55 | jdbc:ascendix:salesforce://;user=myname@companyorg.com.xre.ci;password=passwordandsecretkey 56 | ``` 57 | An example for a connection URL with _sessionId_ parameter: 58 | ``` 59 | jdbc:ascendix:salesforce://;sessionId=uniqueIdAssociatedWithTheSession 60 | ``` 61 | ### Configuration Properties 62 | | Property | Description | 63 | | --- | --- | 64 | | _user_ | Login username. | 65 | | _password_ | Login password is associated with the specified username.
**Warning!** A password provided should contain your password and secret key joined in one string.| 66 | | _sessionId_ | Unique ID associated with this session. | 67 | | _loginDomain_ | Top-level domain for a login request.
Default value is _login.salesforce.com_.
Set _test.salesforce.com_ value to use sandbox. | 68 | | _https_ | Switch to use HTTP protocol instead of HTTPS
Default value is _true_| 69 | | _api_ | Api version to use.
Default value is _50.0_.
Set _test.salesforce.com_ value to use sandbox. | 70 | | _client_ | Client Id to use.
Default value is empty. | 71 | 72 | 73 | ## Supported features 74 | 1. Queries support native SOQL; 75 | 2. Nested queries are supported; 76 | 3. Request caching support on local drive. Caching supports 2 modes: global and session. Global mode means that the cached result will be accessible for all system users for certain JVM session. Session cache mode works for each Salesforce connection session separately. Both modes cache stores request result while JVM still running but no longer than for 1 hour. The cache mode can be enabled with a prefix of SOQL query. How to use: 77 | ======= 78 | * Global cache mode: 79 | ```SQL 80 | CACHE GLOBAL SELECT Id, Name FROM Account 81 | ``` 82 | * Session cache mode 83 | ```SQL 84 | CACHE SESSION SELECT Id, Name FROM Account 85 | ``` 86 | 87 | ## Limitations 88 | 1. The driver is only for read-only purposes now. Insert/udate/delete functionality is not implemented yet. 89 | 90 | ## Configure BIRT Studio to use Salesforce JDBC driver 91 | 92 | 1. [How to add a JDBC driver](https://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.birt.doc%2Fbirt%2Fcon-HowToAddAJDBCDriver.html) 93 | 2. How to set configuration properties for Salesforce JDBC driver. 94 | 95 | Birt provides various ways to set parameters for JDBC driver. For example, it can be done with the property binding feature in the data source editor and a report parameter. 96 | 97 | ![image](/docs/birt/Data%20source%20-%20property%20binding.png) 98 | 99 | See how it's done in [Salesforce JDBC report sample](docs/birt/Salesforce JDBC sample.rptdesign) 100 | 101 | 102 | 103 | ### Sponsors 104 | [Ascendix Technologies Inc.](https://ascendix.com/) 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /docs/birt/Data source - property binding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascendix/salesforce-jdbc/c3d5d39e530f83600addbc4d98e7b040ef8a32cc/docs/birt/Data source - property binding.png -------------------------------------------------------------------------------- /docs/birt/Salesforce JDBC sample.rptdesign: -------------------------------------------------------------------------------- 1 | 2 | 3 | Eclipse BIRT Designer Version 4.7.0.v201706222054 4 | 5 | 6 | odaDriverClass 7 | 4 8 | 9 | 10 | odaURL 11 | 4 12 | "jdbc:ascendix:salesforce://;sessionId=" + params["sessionId"].value; 13 | 14 | 15 | odaUser 16 | 4 17 | 18 | 19 | odaAutoCommit 20 | 4 21 | 22 | 23 | odaIsolationMode 24 | 4 25 | 26 | 27 | odaPassword 28 | 4 29 | 30 | 31 | odaJndiName 32 | 4 33 | 34 | 35 | OdaConnProfileName 36 | 4 37 | 38 | 39 | OdaConnProfileStorePath 40 | 4 41 | 42 | 43 | in 44 | /templates/blank_report.gif 45 | ltr 46 | 96 47 | 48 | 49 | true 50 | static 51 | false 52 | string 53 | true 54 | 55 | uniqueIdAssociatedWithTheSession 56 | 57 | 58 | 59 | uniqueIdAssociatedWithTheSession 60 | 61 | 62 | simple 63 | list-box 64 | true 65 | true 66 | 67 | Unformatted 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | metadataBidiFormatStr 82 | ILYNN 83 | 84 | 85 | disabledMetadataBidiFormatStr 86 | 87 | 88 | contentBidiFormatStr 89 | ILYNN 90 | 91 | 92 | disabledContentBidiFormatStr 93 | 94 | 95 | com.ascendix.jdbc.salesforce.ForceDriver 96 | jdbc:ascendix:salesforce:// 97 | 98 | 99 | 100 | 101 | 102 | 103 | Id 104 | dimension 105 | Id 106 | Id 107 | 108 | 109 | Name 110 | dimension 111 | Name 112 | Name 113 | 114 | 115 | 116 | 117 | 118 | 119 | 1 120 | Id 121 | string 122 | 123 | 124 | 2 125 | Name 126 | string 127 | 128 | 129 | 130 | SF Data Source 131 | 132 | 133 | 1 134 | Id 135 | Id 136 | string 137 | 12 138 | 139 | 140 | 2 141 | Name 142 | Name 143 | string 144 | 12 145 | 146 | 147 | 150 | 151 | 152 | 2.0 153 | 154 | 155 | 156 | 157 | 158 | 159 | Id 160 | 1 161 | 162 | 12 163 | 2147483647 164 | 0 165 | NotNullable 166 | 167 | Id 168 | 169 | 170 | 171 | Id 172 | 173 | 0 174 | 175 | 176 | 177 | 178 | 179 | 180 | Name 181 | 2 182 | 183 | 12 184 | 2147483647 185 | 0 186 | NotNullable 187 | 188 | Name 189 | 190 | 191 | 192 | Name 193 | 194 | 0 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | ]]> 203 | 204 | 205 | 206 | 207 | 208 | 209 | 213 | 214 | Account 215 | 216 | 217 | Id 218 | Id 219 | dataSetRow["Id"] 220 | string 221 | 222 | 223 | Name 224 | Name 225 | dataSetRow["Name"] 226 | string 227 | 228 | 229 | 230 | 231 |
232 | 233 | 234 | solid 235 | 1px 236 | solid 237 | 1px 238 | solid 239 | 1px 240 | solid 241 | 1px 242 | 245 | 246 | 247 | solid 248 | 1px 249 | solid 250 | 1px 251 | solid 252 | 1px 253 | solid 254 | 1px 255 | 258 | 259 | 260 |
261 | 262 | 263 | 264 | solid 265 | 1px 266 | solid 267 | 1px 268 | solid 269 | 1px 270 | solid 271 | 1px 272 | 273 | Id 274 | 275 | 276 | 277 | solid 278 | 1px 279 | solid 280 | 1px 281 | solid 282 | 1px 283 | solid 284 | 1px 285 | 286 | Name 287 | 288 | 289 | 290 | 291 |
292 | 293 |
294 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.ascendix.salesforce 5 | salesforce-jdbc 6 | 1.2-SNAPSHOT 7 | pom 8 | 9 | 10 | sf-auth-client 11 | sf-jdbc-driver 12 | 13 | 14 | 15 | 1.8 16 | 1.8 17 | UTF-8 18 | 19 | 1.18.0 20 | 1.7.25 21 | 1.25.0 22 | 3.8 23 | 4.1 24 | 25 | 4.13.1 26 | 1.3 27 | 28 | 3.7.0 29 | 2.5 30 | 2.21.0 31 | 3.0.1 32 | 3.1.0 33 | 3.1.0 34 | 2.8.2 35 | 2.0 36 | 50.0.0 37 | 3.0.5 38 | 3.5.2 39 | 4.2 40 | 1.4.13-java7 41 | 2.4 42 | 43 | 44 | 45 | 46 | acx.com-maven-repo 47 | default_url 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.projectlombok 55 | lombok 56 | ${lombok.version} 57 | 58 | 59 | org.slf4j 60 | slf4j-api 61 | ${slf4j.version} 62 | 63 | 64 | 65 | com.google.oauth-client 66 | google-oauth-client 67 | ${google-oauth-client.version} 68 | 69 | 70 | org.apache.httpcomponents 71 | httpclient 72 | 73 | 74 | 75 | 76 | com.google.http-client 77 | google-http-client-jackson2 78 | ${google-oauth-client.version} 79 | 80 | 81 | 82 | com.ascendix.salesforce 83 | sf-auth-client 84 | ${project.parent.version} 85 | 86 | 87 | 88 | org.mule.tools 89 | salesforce-soql-parser 90 | ${salesforce-soql-parser.version} 91 | 92 | 93 | com.force.api 94 | force-partner-api 95 | ${force-partner-api.version} 96 | 97 | 98 | org.mapdb 99 | mapdb 100 | ${mapdb.version} 101 | 102 | 103 | org.apache.commons 104 | commons-lang3 105 | ${commons-lang3.version} 106 | 107 | 108 | org.antlr 109 | antlr 110 | ${antlr.version} 111 | 112 | 113 | org.apache.commons 114 | commons-collections4 115 | ${commons-collections4.version} 116 | 117 | 118 | org.apache.directory.studio 119 | org.apache.commons.io 120 | ${org.apache.commons.io.version} 121 | 122 | 123 | 124 | 125 | junit 126 | junit 127 | ${junit.version} 128 | test 129 | 130 | 131 | com.opencsv 132 | opencsv 133 | ${opencsv.version} 134 | test 135 | 136 | 137 | com.thoughtworks.xstream 138 | xstream 139 | ${xstream.version} 140 | test 141 | 142 | 143 | 144 | 145 | 146 | 147 | org.projectlombok 148 | lombok 149 | provided 150 | 151 | 152 | org.slf4j 153 | slf4j-api 154 | 155 | 156 | com.google.oauth-client 157 | google-oauth-client 158 | 159 | 160 | com.google.http-client 161 | google-http-client-jackson2 162 | 163 | 164 | 165 | 166 | 167 | mulesoft-releases 168 | MuleSoft Releases Repository 169 | http://repository.mulesoft.org/releases/ 170 | default 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-compiler-plugin 180 | ${maven-compiler-plugin.version} 181 | 182 | 183 | org.apache.maven.plugins 184 | maven-surefire-plugin 185 | ${maven-surefire-plugin.version} 186 | 187 | 188 | org.apache.maven.plugins 189 | maven-jar-plugin 190 | ${maven-jar-plugin.version} 191 | 192 | 193 | org.codehaus.mojo 194 | versions-maven-plugin 195 | ${versions-maven-plugin.version} 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-assembly-plugin 200 | ${maven-assembly-plugin.version} 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-source-plugin 205 | ${maven-source-plugin.version} 206 | 207 | 208 | org.apache.maven.plugins 209 | maven-deploy-plugin 210 | ${maven-deploy-plugin.version} 211 | 212 | true 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-source-plugin 222 | 223 | 224 | attach-sources 225 | 226 | jar 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /sf-auth-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | salesforce-jdbc 8 | com.ascendix.salesforce 9 | 1.2-SNAPSHOT 10 | 11 | 12 | sf-auth-client 13 | 14 | 15 | 16 | org.apache.commons 17 | commons-lang3 18 | 19 | 20 | org.apache.directory.studio 21 | org.apache.commons.io 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sf-auth-client/src/main/java/com/ascendix/salesforce/oauth/BadOAuthTokenException.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.salesforce.oauth; 2 | 3 | public class BadOAuthTokenException extends RuntimeException { 4 | 5 | public BadOAuthTokenException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sf-auth-client/src/main/java/com/ascendix/salesforce/oauth/ForceClientException.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.salesforce.oauth; 2 | 3 | public class ForceClientException extends RuntimeException { 4 | 5 | public ForceClientException(String message) { 6 | super(message); 7 | } 8 | 9 | public ForceClientException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sf-auth-client/src/main/java/com/ascendix/salesforce/oauth/ForceOAuthClient.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.salesforce.oauth; 2 | 3 | import com.google.api.client.auth.oauth2.BearerToken; 4 | import com.google.api.client.auth.oauth2.Credential; 5 | import com.google.api.client.http.GenericUrl; 6 | import com.google.api.client.http.HttpBackOffIOExceptionHandler; 7 | import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; 8 | import com.google.api.client.http.HttpIOExceptionHandler; 9 | import com.google.api.client.http.HttpRequestFactory; 10 | import com.google.api.client.http.HttpResponse; 11 | import com.google.api.client.http.HttpResponseException; 12 | import com.google.api.client.http.HttpStatusCodes; 13 | import com.google.api.client.http.HttpTransport; 14 | import com.google.api.client.http.javanet.NetHttpTransport; 15 | import com.google.api.client.json.JsonFactory; 16 | import com.google.api.client.json.jackson2.JacksonFactory; 17 | import com.google.api.client.util.BackOff; 18 | import com.google.api.client.util.ExponentialBackOff; 19 | import lombok.extern.slf4j.Slf4j; 20 | import org.apache.commons.lang3.StringUtils; 21 | 22 | import java.io.IOException; 23 | 24 | @Slf4j 25 | public class ForceOAuthClient { 26 | 27 | private static final String LOGIN_URL = "https://login.salesforce.com/services/oauth2/userinfo"; 28 | private static final String TEST_LOGIN_URL = "https://test.salesforce.com/services/oauth2/userinfo"; 29 | private static final String API_VERSION = "43"; 30 | 31 | private static final String BAD_TOKEN_SF_ERROR_CODE = "Bad_OAuth_Token"; 32 | private static final String MISSING_TOKEN_SF_ERROR_CODE = "Missing_OAuth_Token"; 33 | private static final String WRONG_ORG_SF_ERROR_CODE = "Wrong_Org"; 34 | private static final String BAD_ID_SF_ERROR_CODE = "Bad_Id"; 35 | private static final String INTERNAL_SERVER_ERROR_SF_ERROR_CODE = "Internal Error"; 36 | private static final int MAX_RETRIES = 5; 37 | 38 | private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 39 | private static final JsonFactory JSON_FACTORY = new JacksonFactory(); 40 | 41 | private final long connectTimeout; 42 | private final long readTimeout; 43 | 44 | public ForceOAuthClient(long connectTimeout, long readTimeout) { 45 | this.connectTimeout = connectTimeout; 46 | this.readTimeout = readTimeout; 47 | } 48 | 49 | public ForceUserInfo getUserInfo(String accessToken, boolean sandbox) { 50 | GenericUrl loginUrl = new GenericUrl(sandbox ? TEST_LOGIN_URL : LOGIN_URL); 51 | HttpRequestFactory requestFactory = buildHttpRequestFactory(accessToken); 52 | int tryCount = 0; 53 | while (true) { 54 | try { 55 | HttpResponse result = requestFactory.buildGetRequest(loginUrl).execute(); 56 | ForceUserInfo forceUserInfo = result.parseAs(ForceUserInfo.class); 57 | extractPartnerUrl(forceUserInfo); 58 | extractInstance(forceUserInfo); 59 | 60 | return forceUserInfo; 61 | } catch (HttpResponseException e) { 62 | if (isForceInternalError(e) && tryCount < MAX_RETRIES) { 63 | tryCount++; 64 | continue; //try one more time 65 | } 66 | if (isBadTokenError(e)) { 67 | throw new BadOAuthTokenException("Bad OAuth Token: " + accessToken); 68 | } 69 | throw new ForceClientException("Response error: " + e.getStatusCode() + " " + e.getContent()); 70 | } catch (IOException e) { 71 | throw new ForceClientException("IO error: " + e.getMessage(), e); 72 | } 73 | } 74 | } 75 | 76 | private HttpRequestFactory buildHttpRequestFactory(String accessToken) { 77 | Credential credential = new Credential(BearerToken.authorizationHeaderAccessMethod()) 78 | .setAccessToken(accessToken); 79 | 80 | return HTTP_TRANSPORT.createRequestFactory( 81 | request -> { 82 | request.setConnectTimeout(Math.toIntExact(connectTimeout)); 83 | request.setReadTimeout(Math.toIntExact(readTimeout)); 84 | request.setParser(JSON_FACTORY.createJsonObjectParser()); 85 | request.setInterceptor(credential); 86 | request.setUnsuccessfulResponseHandler(buildUnsuccessfulResponseHandler()); 87 | request.setIOExceptionHandler(buildIOExceptionHandler()); 88 | request.setNumberOfRetries(MAX_RETRIES); 89 | }); 90 | } 91 | 92 | 93 | private static void extractPartnerUrl(ForceUserInfo userInfo) { 94 | if (userInfo.getUrls() == null || !userInfo.getUrls().containsKey("partner")) { 95 | throw new IllegalStateException("User info doesn't contain partner URL: " + userInfo.getUrls()); 96 | } 97 | userInfo.setPartnerUrl(userInfo.getUrls().get("partner").replace("{version}", API_VERSION)); 98 | } 99 | 100 | private boolean isBadTokenError(HttpResponseException e) { 101 | return ((e.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) 102 | && StringUtils.equalsAnyIgnoreCase(e.getContent(), 103 | BAD_TOKEN_SF_ERROR_CODE, MISSING_TOKEN_SF_ERROR_CODE, WRONG_ORG_SF_ERROR_CODE)) 104 | || 105 | (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND && 106 | StringUtils.equalsIgnoreCase(e.getContent(), BAD_ID_SF_ERROR_CODE)); 107 | } 108 | 109 | private boolean isForceInternalError(HttpResponseException e) { 110 | return e.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND && 111 | StringUtils.equalsIgnoreCase(e.getContent(), INTERNAL_SERVER_ERROR_SF_ERROR_CODE); 112 | } 113 | 114 | private BackOff getBackOff() { 115 | return new ExponentialBackOff.Builder() 116 | .setInitialIntervalMillis(500) 117 | .setMaxElapsedTimeMillis(30000) 118 | .setMaxIntervalMillis(10000) 119 | .setMultiplier(1.5) 120 | .setRandomizationFactor(0.5) 121 | .build(); 122 | } 123 | 124 | private HttpBackOffUnsuccessfulResponseHandler buildUnsuccessfulResponseHandler() { 125 | return new HttpBackOffUnsuccessfulResponseHandler(getBackOff()); 126 | } 127 | 128 | private HttpIOExceptionHandler buildIOExceptionHandler() { 129 | return new HttpBackOffIOExceptionHandler(getBackOff()); 130 | } 131 | 132 | private static void extractInstance(ForceUserInfo userInfo) { 133 | String profileUrl = userInfo.getPartnerUrl(); 134 | if (StringUtils.isBlank(profileUrl)) { 135 | return; 136 | } 137 | profileUrl = profileUrl.replace("https://", ""); 138 | try { 139 | String instance = StringUtils.split(profileUrl, '.')[0]; 140 | userInfo.setInstance(instance); 141 | } catch (Exception e) { 142 | log.error("Failed to parse instance name from profile: {}", profileUrl, e); 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /sf-auth-client/src/main/java/com/ascendix/salesforce/oauth/ForceUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.salesforce.oauth; 2 | 3 | import com.google.api.client.util.Key; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.Map; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | public class ForceUserInfo { 12 | 13 | @Key("user_id") 14 | private String userId; 15 | @Key("organization_id") 16 | private String organizationId; 17 | @Key("preferred_username") 18 | private String preferredUsername; 19 | @Key("nickname") 20 | private String nickName; 21 | private String name; 22 | private String email; 23 | @Key("zoneinfo") 24 | private String timeZone; 25 | @Key("locale") 26 | private String locale; 27 | private String instance; 28 | private String partnerUrl; 29 | @Key("urls") 30 | private Map urls; 31 | } 32 | -------------------------------------------------------------------------------- /sf-auth-client/src/main/java/com/ascendix/salesforce/soap/ForceSoapValidator.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.salesforce.soap; 2 | 3 | import com.ascendix.salesforce.oauth.ForceClientException; 4 | import com.google.api.client.http.ByteArrayContent; 5 | import com.google.api.client.http.GenericUrl; 6 | import com.google.api.client.http.HttpHeaders; 7 | import com.google.api.client.http.HttpRequest; 8 | import com.google.api.client.http.HttpRequestFactory; 9 | import com.google.api.client.http.HttpResponse; 10 | import com.google.api.client.http.HttpResponseException; 11 | import com.google.api.client.http.HttpStatusCodes; 12 | import com.google.api.client.http.HttpTransport; 13 | import com.google.api.client.http.javanet.NetHttpTransport; 14 | import org.apache.commons.io.IOUtils; 15 | import org.apache.commons.lang3.StringUtils; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | 20 | public class ForceSoapValidator { 21 | private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 22 | private final long connectTimeout; 23 | private final long readTimeout; 24 | 25 | private static final String SOAP_FAULT = ""; 26 | private static final String BAD_TOKEN_SF_ERROR_CODE = "INVALID_SESSION_ID"; 27 | 28 | public ForceSoapValidator(long connectTimeout, long readTimeout) { 29 | this.connectTimeout = connectTimeout; 30 | this.readTimeout = readTimeout; 31 | } 32 | 33 | public boolean validateForceToken(String partnerUrl, String accessToken) { 34 | HttpRequestFactory requestFactory = buildHttpRequestFactory(); 35 | ClassLoader classLoader = getClass().getClassLoader(); 36 | try (InputStream is = classLoader.getResourceAsStream("forceSoapBody")) { 37 | 38 | String requestBody = IOUtils.toString(is); 39 | 40 | HttpRequest request = requestFactory.buildPostRequest( 41 | new GenericUrl(partnerUrl), 42 | ByteArrayContent.fromString( 43 | "text/xml", 44 | requestBody.replace("{sessionId}", accessToken) 45 | )); 46 | HttpHeaders headers = request.getHeaders(); 47 | headers.set("SOAPAction", "some"); 48 | HttpResponse result = request.execute(); 49 | return result.getStatusCode() == HttpStatusCodes.STATUS_CODE_OK; 50 | } catch (HttpResponseException e) { 51 | if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_SERVER_ERROR && 52 | StringUtils.containsIgnoreCase(e.getContent(), SOAP_FAULT) && 53 | StringUtils.containsIgnoreCase(e.getContent(), BAD_TOKEN_SF_ERROR_CODE)) { 54 | return false; 55 | } 56 | throw new ForceClientException("Response error: " + e.getStatusCode() + " " + e.getContent()); 57 | } catch (IOException e) { 58 | throw new ForceClientException("IO error: " + e.getMessage(), e); 59 | } 60 | } 61 | 62 | private HttpRequestFactory buildHttpRequestFactory() { 63 | return HTTP_TRANSPORT.createRequestFactory( 64 | request -> { 65 | request.setConnectTimeout(Math.toIntExact(connectTimeout)); 66 | request.setReadTimeout(Math.toIntExact(readTimeout)); 67 | }); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /sf-auth-client/src/main/resources/forceSoapBody: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | {sessionId} 6 | 7 | 8 | 9 | 10 | Select Id From User limit 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /sf-auth-client/src/test/java/util/DBTablePrinter.java: -------------------------------------------------------------------------------- 1 | /* 2 | Database Table Printer 3 | Copyright (C) 2014 Hami Galip Torun 4 | 5 | Email: hamitorun@e-fabrika.net 6 | Project Home: https://github.com/htorun/dbtableprinter 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | /* 23 | This is my first Java program that does something more or less 24 | useful. It is part of my effort to learn Java, how to use 25 | an IDE (IntelliJ IDEA 13.1.15 in this case), how to apply an 26 | open source license and how to use Git and GitHub (https://github.com) 27 | for version control and publishing an open source software. 28 | 29 | Hami 30 | */ 31 | 32 | package net.efabrika.util; 33 | 34 | import java.sql.Connection; 35 | import java.sql.ResultSet; 36 | import java.sql.ResultSetMetaData; 37 | import java.sql.SQLException; 38 | import java.sql.Statement; 39 | import java.sql.Types; 40 | import java.util.ArrayList; 41 | import java.util.List; 42 | import java.util.StringJoiner; 43 | 44 | /** 45 | * Just a utility to print rows from a given DB table or a 46 | * ResultSet to standard out, formatted to look 47 | * like a table with rows and columns with borders. 48 | * 49 | *

Stack Overflow website 50 | * (stackoverflow.com) 51 | * was the primary source of inspiration and help to put this 52 | * code together. Especially the questions and answers of 53 | * the following people were very useful:

54 | * 55 | *

Question: 56 | * How to display or 57 | * print the contents of a database table as is
58 | * People: sky scraper

59 | * 60 | *

Question: 61 | * System.out.println() 62 | * from database into a table
63 | * People: Simon Cottrill, Tony Toews, Costis Aivali, Riggy, corsiKa

64 | * 65 | *

Question: 66 | * Simple way to repeat 67 | * a string in java
68 | * People: Everybody who contributed but especially user102008

69 | */ 70 | public class DBTablePrinter { 71 | 72 | /** 73 | * Default maximum number of rows to query and print. 74 | */ 75 | private static final int DEFAULT_MAX_ROWS = 10; 76 | 77 | /** 78 | * Default maximum width for text columns 79 | * (like a VARCHAR) column. 80 | */ 81 | private static final int DEFAULT_MAX_TEXT_COL_WIDTH = 150; 82 | 83 | /** 84 | * Column type category for CHAR, VARCHAR 85 | * and similar text columns. 86 | */ 87 | public static final int CATEGORY_STRING = 1; 88 | 89 | /** 90 | * Column type category for TINYINT, SMALLINT, 91 | * INT and BIGINT columns. 92 | */ 93 | public static final int CATEGORY_INTEGER = 2; 94 | 95 | /** 96 | * Column type category for REAL, DOUBLE, 97 | * and DECIMAL columns. 98 | */ 99 | public static final int CATEGORY_DOUBLE = 3; 100 | 101 | /** 102 | * Column type category for date and time related columns like 103 | * DATE, TIME, TIMESTAMP etc. 104 | */ 105 | public static final int CATEGORY_DATETIME = 4; 106 | 107 | /** 108 | * Column type category for BOOLEAN columns. 109 | */ 110 | public static final int CATEGORY_BOOLEAN = 5; 111 | 112 | /** 113 | * Column type category for types for which the type name 114 | * will be printed instead of the content, like BLOB, 115 | * BINARY, ARRAY etc. 116 | */ 117 | public static final int CATEGORY_OTHER = 0; 118 | 119 | /** 120 | * Represents a database table column. 121 | */ 122 | private static class Column { 123 | 124 | /** 125 | * Column label. 126 | */ 127 | private String label; 128 | 129 | /** 130 | * Generic SQL type of the column as defined in 131 | * 133 | * java.sql.Types 134 | * . 135 | */ 136 | private int type; 137 | 138 | /** 139 | * Generic SQL type name of the column as defined in 140 | * 142 | * java.sql.Types 143 | * . 144 | */ 145 | private String typeName; 146 | 147 | /** 148 | * Width of the column that will be adjusted according to column label 149 | * and values to be printed. 150 | */ 151 | private int width = 0; 152 | 153 | /** 154 | * Column values from each row of a ResultSet. 155 | */ 156 | private List values = new ArrayList<>(); 157 | 158 | /** 159 | * Flag for text justification using String.format. 160 | * Empty string ("") to justify right, 161 | * dash (-) to justify left. 162 | * 163 | * @see #justifyLeft() 164 | */ 165 | private String justifyFlag = ""; 166 | 167 | /** 168 | * Column type category. The columns will be categorised according 169 | * to their column types and specific needs to print them correctly. 170 | */ 171 | private int typeCategory = 0; 172 | 173 | /** 174 | * Constructs a new Column with a column label, 175 | * generic SQL type and type name (as defined in 176 | * 178 | * java.sql.Types 179 | * ) 180 | * 181 | * @param label Column label or name 182 | * @param type Generic SQL type 183 | * @param typeName Generic SQL type name 184 | */ 185 | public Column(String label, int type, String typeName) { 186 | this.label = label; 187 | this.type = type; 188 | this.typeName = typeName; 189 | } 190 | 191 | /** 192 | * Returns the column label 193 | * 194 | * @return Column label 195 | */ 196 | public String getLabel() { 197 | return label; 198 | } 199 | 200 | /** 201 | * Returns the generic SQL type of the column 202 | * 203 | * @return Generic SQL type 204 | */ 205 | public int getType() { 206 | return type; 207 | } 208 | 209 | /** 210 | * Returns the generic SQL type name of the column 211 | * 212 | * @return Generic SQL type name 213 | */ 214 | public String getTypeName() { 215 | return typeName; 216 | } 217 | 218 | /** 219 | * Returns the width of the column 220 | * 221 | * @return Column width 222 | */ 223 | public int getWidth() { 224 | return width; 225 | } 226 | 227 | /** 228 | * Sets the width of the column to width 229 | * 230 | * @param width Width of the column 231 | */ 232 | public void setWidth(int width) { 233 | this.width = width; 234 | } 235 | 236 | /** 237 | * Adds a String representation (value) 238 | * of a value to this column object's {@link #values} list. 239 | * These values will come from each row of a 240 | * 242 | * ResultSet 243 | * of a database query. 244 | * 245 | * @param value The column value to add to {@link #values} 246 | */ 247 | public void addValue(String value) { 248 | values.add(value); 249 | } 250 | 251 | /** 252 | * Returns the column value at row index i. 253 | * Note that the index starts at 0 so that getValue(0) 254 | * will get the value for this column from the first row 255 | * of a 257 | * ResultSet. 258 | * 259 | * @param i The index of the column value to get 260 | * @return The String representation of the value 261 | */ 262 | public String getValue(int i) { 263 | return values.get(i); 264 | } 265 | 266 | /** 267 | * Returns the value of the {@link #justifyFlag}. The column 268 | * values will be printed using String.format and 269 | * this flag will be used to right or left justify the text. 270 | * 271 | * @return The {@link #justifyFlag} of this column 272 | * @see #justifyLeft() 273 | */ 274 | public String getJustifyFlag() { 275 | return justifyFlag; 276 | } 277 | 278 | /** 279 | * Sets {@link #justifyFlag} to "-" so that 280 | * the column value will be left justified when printed with 281 | * String.format. Typically numbers will be right 282 | * justified and text will be left justified. 283 | */ 284 | public void justifyLeft() { 285 | this.justifyFlag = "-"; 286 | } 287 | 288 | /** 289 | * Returns the generic SQL type category of the column 290 | * 291 | * @return The {@link #typeCategory} of the column 292 | */ 293 | public int getTypeCategory() { 294 | return typeCategory; 295 | } 296 | 297 | /** 298 | * Sets the {@link #typeCategory} of the column 299 | * 300 | * @param typeCategory The type category 301 | */ 302 | public void setTypeCategory(int typeCategory) { 303 | this.typeCategory = typeCategory; 304 | } 305 | } 306 | 307 | /** 308 | * Overloaded method that prints rows from table tableName 309 | * to standard out using the given database connection 310 | * conn. Total number of rows will be limited to 311 | * {@link #DEFAULT_MAX_ROWS} and 312 | * {@link #DEFAULT_MAX_TEXT_COL_WIDTH} will be used to limit 313 | * the width of text columns (like a VARCHAR column). 314 | * 315 | * @param conn Database connection object (java.sql.Connection) 316 | * @param tableName Name of the database table 317 | */ 318 | public static void printTable(Connection conn, String tableName) { 319 | printTable(conn, tableName, DEFAULT_MAX_ROWS, DEFAULT_MAX_TEXT_COL_WIDTH); 320 | } 321 | 322 | /** 323 | * Overloaded method that prints rows from table tableName 324 | * to standard out using the given database connection 325 | * conn. Total number of rows will be limited to 326 | * maxRows and 327 | * {@link #DEFAULT_MAX_TEXT_COL_WIDTH} will be used to limit 328 | * the width of text columns (like a VARCHAR column). 329 | * 330 | * @param conn Database connection object (java.sql.Connection) 331 | * @param tableName Name of the database table 332 | * @param maxRows Number of max. rows to query and print 333 | */ 334 | public static void printTable(Connection conn, String tableName, int maxRows) { 335 | printTable(conn, tableName, maxRows, DEFAULT_MAX_TEXT_COL_WIDTH); 336 | } 337 | 338 | /** 339 | * Overloaded method that prints rows from table tableName 340 | * to standard out using the given database connection 341 | * conn. Total number of rows will be limited to 342 | * maxRows and 343 | * maxStringColWidth will be used to limit 344 | * the width of text columns (like a VARCHAR column). 345 | * 346 | * @param conn Database connection object (java.sql.Connection) 347 | * @param tableName Name of the database table 348 | * @param maxRows Number of max. rows to query and print 349 | * @param maxStringColWidth Max. width of text columns 350 | */ 351 | public static void printTable(Connection conn, String tableName, int maxRows, int maxStringColWidth) { 352 | if (conn == null) { 353 | System.err.println("DBTablePrinter Error: No connection to database (Connection is null)!"); 354 | return; 355 | } 356 | if (tableName == null) { 357 | System.err.println("DBTablePrinter Error: No table name (tableName is null)!"); 358 | return; 359 | } 360 | if (tableName.length() == 0) { 361 | System.err.println("DBTablePrinter Error: Empty table name!"); 362 | return; 363 | } 364 | if (maxRows < 1) { 365 | System.err.println("DBTablePrinter Info: Invalid max. rows number. Using default!"); 366 | maxRows = DEFAULT_MAX_ROWS; 367 | } 368 | 369 | Statement stmt = null; 370 | ResultSet rs = null; 371 | try { 372 | if (conn.isClosed()) { 373 | System.err.println("DBTablePrinter Error: Connection is closed!"); 374 | return; 375 | } 376 | 377 | String sqlSelectAll = "SELECT * FROM " + tableName + " LIMIT " + maxRows; 378 | stmt = conn.createStatement(); 379 | rs = stmt.executeQuery(sqlSelectAll); 380 | 381 | printResultSet(rs, maxStringColWidth); 382 | 383 | } catch (SQLException e) { 384 | System.err.println("SQL exception in DBTablePrinter. Message:"); 385 | System.err.println(e.getMessage()); 386 | } finally { 387 | try { 388 | if (stmt != null) { 389 | stmt.close(); 390 | } 391 | if (rs != null) { 392 | rs.close(); 393 | } 394 | } catch (SQLException ignore) { 395 | // ignore 396 | } 397 | } 398 | } 399 | 400 | /** 401 | * Overloaded method to print rows of a 403 | * ResultSet to standard out using {@link #DEFAULT_MAX_TEXT_COL_WIDTH} 404 | * to limit the width of text columns. 405 | * 406 | * @param rs The ResultSet to print 407 | */ 408 | public static void printResultSet(ResultSet rs) { 409 | printResultSet(rs, DEFAULT_MAX_TEXT_COL_WIDTH); 410 | } 411 | 412 | /** 413 | * Overloaded method to print rows of a 415 | * ResultSet to standard out using maxStringColWidth 416 | * to limit the width of text columns. 417 | * 418 | * @param rs The ResultSet to print 419 | * @param maxStringColWidth Max. width of text columns 420 | */ 421 | public static void printResultSet(ResultSet rs, int maxStringColWidth) { 422 | try { 423 | if (rs == null) { 424 | System.err.println("DBTablePrinter Error: Result set is null!"); 425 | return; 426 | } 427 | if (rs.isClosed()) { 428 | System.err.println("DBTablePrinter Error: Result Set is closed!"); 429 | return; 430 | } 431 | if (maxStringColWidth < 1) { 432 | System.err.println("DBTablePrinter Info: Invalid max. varchar column width. Using default!"); 433 | maxStringColWidth = DEFAULT_MAX_TEXT_COL_WIDTH; 434 | } 435 | 436 | // Get the meta data object of this ResultSet. 437 | ResultSetMetaData rsmd; 438 | rsmd = rs.getMetaData(); 439 | 440 | // Total number of columns in this ResultSet 441 | int columnCount = rsmd.getColumnCount(); 442 | 443 | // List of Column objects to store each columns of the ResultSet 444 | // and the String representation of their values. 445 | List columns = new ArrayList<>(columnCount); 446 | 447 | // List of table names. Can be more than one if it is a joined 448 | // table query 449 | List tableNames = new ArrayList<>(columnCount); 450 | 451 | // Get the columns and their meta data. 452 | // NOTE: columnIndex for rsmd.getXXX methods STARTS AT 1 NOT 0 453 | for (int i = 1; i <= columnCount; i++) { 454 | Column c = new Column(rsmd.getColumnLabel(i), 455 | rsmd.getColumnType(i), rsmd.getColumnTypeName(i)); 456 | c.setWidth(c.getLabel().length()); 457 | c.setTypeCategory(whichCategory(c.getType())); 458 | columns.add(c); 459 | 460 | if (!tableNames.contains(rsmd.getTableName(i))) { 461 | tableNames.add(rsmd.getTableName(i)); 462 | } 463 | } 464 | 465 | // Go through each row, get values of each column and adjust 466 | // column widths. 467 | int rowCount = 0; 468 | while (rs.next()) { 469 | 470 | System.out.println("row: " + rowCount); 471 | 472 | // NOTE: columnIndex for rs.getXXX methods STARTS AT 1 NOT 0 473 | for (int i = 0; i < columnCount; i++) { 474 | Column c = columns.get(i); 475 | String value; 476 | int category = c.getTypeCategory(); 477 | 478 | if (category == CATEGORY_OTHER) { 479 | 480 | // Use generic SQL type name instead of the actual value 481 | // for column types BLOB, BINARY etc. 482 | value = "(" + c.getTypeName() + ")"; 483 | 484 | } else { 485 | value = rs.getString(i + 1) == null ? "NULL" : rs.getString(i + 1); 486 | } 487 | switch (category) { 488 | case CATEGORY_DOUBLE: 489 | 490 | // For real numbers, format the string value to have 3 digits 491 | // after the point. THIS IS TOTALLY ARBITRARY and can be 492 | // improved to be CONFIGURABLE. 493 | if (!value.equals("NULL")) { 494 | Double dValue = rs.getDouble(i + 1); 495 | value = String.format("%.3f", dValue); 496 | } 497 | break; 498 | 499 | case CATEGORY_STRING: 500 | 501 | // Left justify the text columns 502 | c.justifyLeft(); 503 | 504 | // and apply the width limit 505 | if (value.length() > maxStringColWidth) { 506 | value = value.substring(0, maxStringColWidth - 3) + "..."; 507 | } 508 | break; 509 | } 510 | 511 | // Adjust the column width 512 | c.setWidth(value.length() > c.getWidth() ? value.length() : c.getWidth()); 513 | c.addValue(value); 514 | } // END of for loop columnCount 515 | rowCount++; 516 | 517 | } // END of while (rs.next) 518 | 519 | /* 520 | At this point we have gone through meta data, get the 521 | columns and created all Column objects, iterated over the 522 | ResultSet rows, populated the column values and adjusted 523 | the column widths. 524 | 525 | We cannot start printing just yet because we have to prepare 526 | a row separator String. 527 | */ 528 | 529 | // For the fun of it, I will use StringBuilder 530 | StringBuilder strToPrint = new StringBuilder(); 531 | StringBuilder rowSeparator = new StringBuilder(); 532 | 533 | /* 534 | Prepare column labels to print as well as the row separator. 535 | It should look something like this: 536 | +--------+------------+------------+-----------+ (row separator) 537 | | EMP_NO | BIRTH_DATE | FIRST_NAME | LAST_NAME | (labels row) 538 | +--------+------------+------------+-----------+ (row separator) 539 | */ 540 | 541 | // Iterate over columns 542 | for (Column c : columns) { 543 | int width = c.getWidth(); 544 | 545 | // Center the column label 546 | String toPrint; 547 | String name = c.getLabel(); 548 | int diff = width - name.length(); 549 | 550 | if ((diff % 2) == 1) { 551 | // diff is not divisible by 2, add 1 to width (and diff) 552 | // so that we can have equal padding to the left and right 553 | // of the column label. 554 | width++; 555 | diff++; 556 | c.setWidth(width); 557 | } 558 | 559 | int paddingSize = diff / 2; // InteliJ says casting to int is redundant. 560 | 561 | // Cool String repeater code thanks to user102008 at stackoverflow.com 562 | // (http://tinyurl.com/7x9qtyg) "Simple way to repeat a string in java" 563 | String padding = new String(new char[paddingSize]).replace("\0", " "); 564 | 565 | toPrint = "| " + padding + name + padding + " "; 566 | // END centering the column label 567 | 568 | strToPrint.append(toPrint); 569 | 570 | rowSeparator.append("+"); 571 | rowSeparator.append(new String(new char[width + 2]).replace("\0", "-")); 572 | } 573 | 574 | String lineSeparator = System.getProperty("line.separator"); 575 | 576 | // Is this really necessary ?? 577 | lineSeparator = lineSeparator == null ? "\n" : lineSeparator; 578 | 579 | rowSeparator.append("+").append(lineSeparator); 580 | 581 | strToPrint.append("|").append(lineSeparator); 582 | strToPrint.insert(0, rowSeparator); 583 | strToPrint.append(rowSeparator); 584 | 585 | StringJoiner sj = new StringJoiner(", "); 586 | for (String name : tableNames) { 587 | sj.add(name); 588 | } 589 | 590 | String info = "Printing " + rowCount; 591 | info += rowCount > 1 ? " rows from " : " row from "; 592 | info += tableNames.size() > 1 ? "tables " : "table "; 593 | info += sj.toString(); 594 | 595 | System.out.println(info); 596 | 597 | // Print out the formatted column labels 598 | System.out.print(strToPrint.toString()); 599 | 600 | String format; 601 | 602 | // Print out the rows 603 | for (int i = 0; i < rowCount; i++) { 604 | for (Column c : columns) { 605 | 606 | // This should form a format string like: "%-60s" 607 | format = String.format("| %%%s%ds ", c.getJustifyFlag(), c.getWidth()); 608 | System.out.print( 609 | String.format(format, c.getValue(i)) 610 | ); 611 | } 612 | 613 | System.out.println("|"); 614 | System.out.print(rowSeparator); 615 | } 616 | 617 | System.out.println(); 618 | 619 | /* 620 | Hopefully this should have printed something like this: 621 | +--------+------------+------------+-----------+--------+-------------+ 622 | | EMP_NO | BIRTH_DATE | FIRST_NAME | LAST_NAME | GENDER | HIRE_DATE | 623 | +--------+------------+------------+-----------+--------+-------------+ 624 | | 10001 | 1953-09-02 | Georgi | Facello | M | 1986-06-26 | 625 | +--------+------------+------------+-----------+--------+-------------+ 626 | | 10002 | 1964-06-02 | Bezalel | Simmel | F | 1985-11-21 | 627 | +--------+------------+------------+-----------+--------+-------------+ 628 | */ 629 | 630 | } catch (SQLException e) { 631 | System.err.println("SQL exception in DBTablePrinter. Message:"); 632 | System.err.println(e.getMessage()); 633 | } 634 | } 635 | 636 | /** 637 | * Takes a generic SQL type and returns the category this type 638 | * belongs to. Types are categorized according to print formatting 639 | * needs: 640 | *

641 | * Integers should not be truncated so column widths should 642 | * be adjusted without a column width limit. Text columns should be 643 | * left justified and can be truncated to a max. column width etc...

644 | *

645 | * See also: 647 | * java.sql.Types 648 | * 649 | * @param type Generic SQL type 650 | * @return The category this type belongs to 651 | */ 652 | private static int whichCategory(int type) { 653 | switch (type) { 654 | case Types.BIGINT: 655 | case Types.TINYINT: 656 | case Types.SMALLINT: 657 | case Types.INTEGER: 658 | return CATEGORY_INTEGER; 659 | 660 | case Types.REAL: 661 | case Types.DOUBLE: 662 | case Types.DECIMAL: 663 | return CATEGORY_DOUBLE; 664 | 665 | case Types.DATE: 666 | case Types.TIME: 667 | case Types.TIME_WITH_TIMEZONE: 668 | case Types.TIMESTAMP: 669 | case Types.TIMESTAMP_WITH_TIMEZONE: 670 | return CATEGORY_DATETIME; 671 | 672 | case Types.BOOLEAN: 673 | return CATEGORY_BOOLEAN; 674 | 675 | case Types.VARCHAR: 676 | case Types.NVARCHAR: 677 | case Types.LONGVARCHAR: 678 | case Types.LONGNVARCHAR: 679 | case Types.CHAR: 680 | case Types.NCHAR: 681 | return CATEGORY_STRING; 682 | 683 | default: 684 | return CATEGORY_OTHER; 685 | } 686 | } 687 | } 688 | -------------------------------------------------------------------------------- /sf-jdbc-driver/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | salesforce-jdbc 8 | com.ascendix.salesforce 9 | 1.2-SNAPSHOT 10 | 11 | sf-jdbc-driver 12 | 13 | 14 | 15 | 16 | com.ascendix.salesforce 17 | sf-auth-client 18 | 19 | 20 | org.mule.tools 21 | salesforce-soql-parser 22 | 23 | 24 | com.force.api 25 | force-partner-api 26 | 27 | 28 | org.mapdb 29 | mapdb 30 | 31 | 32 | org.apache.commons 33 | commons-lang3 34 | 35 | 36 | org.antlr 37 | antlr 38 | 39 | 40 | org.apache.commons 41 | commons-collections4 42 | 43 | 44 | 45 | 46 | junit 47 | junit 48 | test 49 | 50 | 51 | com.opencsv 52 | opencsv 53 | test 54 | 55 | 56 | com.thoughtworks.xstream 57 | xstream 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | single-jar 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-assembly-plugin 72 | 73 | 74 | package 75 | 76 | single 77 | 78 | 79 | 80 | 81 | 82 | jar-with-dependencies 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/ForceDriver.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce; 2 | 3 | import com.ascendix.jdbc.salesforce.connection.ForceConnection; 4 | import com.ascendix.jdbc.salesforce.connection.ForceConnectionInfo; 5 | import com.ascendix.jdbc.salesforce.connection.ForceService; 6 | import com.sforce.soap.partner.PartnerConnection; 7 | import com.sforce.ws.ConnectionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.nio.charset.StandardCharsets; 14 | import java.sql.Connection; 15 | import java.sql.Driver; 16 | import java.sql.DriverManager; 17 | import java.sql.DriverPropertyInfo; 18 | import java.sql.SQLException; 19 | import java.sql.SQLFeatureNotSupportedException; 20 | import java.util.Properties; 21 | import java.util.logging.Logger; 22 | import java.util.regex.Matcher; 23 | import java.util.regex.Pattern; 24 | 25 | 26 | @Slf4j 27 | public class ForceDriver implements Driver { 28 | 29 | private static final String ACCEPTABLE_URL = "jdbc:ascendix:salesforce"; 30 | private static final Pattern URL_PATTERN = Pattern.compile("\\A" + ACCEPTABLE_URL + "://(.*)"); 31 | private static final Pattern URL_HAS_AUTHORIZATION_SEGMENT = Pattern.compile("\\A" + ACCEPTABLE_URL + "://([^:]+):([^@]+)@([^?]*)([?](.*))?"); 32 | private static final Pattern PARAM_STANDARD_PATTERN = Pattern.compile("(([^=]+)=([^&]*)&?)"); 33 | 34 | static { 35 | try { 36 | DriverManager.registerDriver(new ForceDriver()); 37 | } catch (Exception e) { 38 | throw new RuntimeException("Failed register ForceDriver: " + e.getMessage(), e); 39 | } 40 | } 41 | 42 | @Override 43 | public Connection connect(String url, Properties properties) throws SQLException { 44 | if (!acceptsURL(url)) { 45 | /* 46 | * According to JDBC spec: 47 | * > The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. 48 | * > This will be common, as when the JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn. 49 | * 50 | * Source: https://docs.oracle.com/javase/8/docs/api/java/sql/Driver.html#connect-java.lang.String-java.util.Properties- 51 | */ 52 | return null; 53 | } 54 | try { 55 | Properties connStringProps = getConnStringProperties(url); 56 | properties.putAll(connStringProps); 57 | ForceConnectionInfo info = new ForceConnectionInfo(); 58 | info.setUserName(properties.getProperty("user")); 59 | info.setClientName(properties.getProperty("client")); 60 | info.setPassword(properties.getProperty("password")); 61 | info.setSessionId(properties.getProperty("sessionId")); 62 | info.setSandbox(resolveSandboxProperty(properties)); 63 | info.setHttps(resolveBooleanProperty(properties, "https", true)); 64 | info.setApiVersion(resolveStringProperty(properties, "api", ForceService.DEFAULT_API_VERSION)); 65 | info.setLoginDomain(resolveStringProperty(properties, "loginDomain", ForceService.DEFAULT_LOGIN_DOMAIN)); 66 | 67 | PartnerConnection partnerConnection = ForceService.createPartnerConnection(info); 68 | return new ForceConnection(partnerConnection); 69 | } catch (ConnectionException | IOException e) { 70 | throw new SQLException(e); 71 | } 72 | } 73 | 74 | private static Boolean resolveSandboxProperty(Properties properties) { 75 | String sandbox = properties.getProperty("sandbox"); 76 | if (sandbox != null) { 77 | return Boolean.valueOf(sandbox); 78 | } 79 | String loginDomain = properties.getProperty("loginDomain"); 80 | if (loginDomain != null) { 81 | return loginDomain.contains("test"); 82 | } 83 | return null; 84 | } 85 | 86 | private static Boolean resolveBooleanProperty(Properties properties, String propertyName, boolean defaultValue) { 87 | String boolValue = properties.getProperty(propertyName); 88 | if (boolValue != null) { 89 | return Boolean.valueOf(boolValue); 90 | } 91 | return defaultValue; 92 | } 93 | 94 | private static String resolveStringProperty(Properties properties, String propertyName, String defaultValue) { 95 | String boolValue = properties.getProperty(propertyName); 96 | if (boolValue != null) { 97 | return boolValue; 98 | } 99 | return defaultValue; 100 | } 101 | 102 | 103 | protected Properties getConnStringProperties(String urlString) throws IOException { 104 | Properties result = new Properties(); 105 | String urlProperties = null; 106 | 107 | Matcher stdMatcher = URL_PATTERN.matcher(urlString); 108 | Matcher authMatcher = URL_HAS_AUTHORIZATION_SEGMENT.matcher(urlString); 109 | 110 | if (authMatcher.matches()) { 111 | result.put("user", authMatcher.group(1)); 112 | result.put("password", authMatcher.group(2)); 113 | result.put("loginDomain", authMatcher.group(3)); 114 | if (authMatcher.groupCount() > 4 && authMatcher.group(5) != null) { 115 | // has some other parameters - parse them from standard URL format like 116 | // ?param1=value1¶m2=value2 117 | String parameters = authMatcher.group(5); 118 | Matcher matcher = PARAM_STANDARD_PATTERN.matcher(parameters); 119 | while(matcher.find()) { 120 | String param = matcher.group(2); 121 | String value = 3 >= matcher.groupCount() ? matcher.group(3) : null; 122 | result.put(param, value); 123 | } 124 | 125 | } 126 | } else if (stdMatcher.matches()) { 127 | urlProperties = stdMatcher.group(1); 128 | urlProperties = urlProperties.replaceAll(";", "\n"); 129 | } 130 | 131 | if (urlProperties != null) { 132 | try (InputStream in = new ByteArrayInputStream(urlProperties.getBytes(StandardCharsets.UTF_8))) { 133 | result.load(in); 134 | } 135 | } 136 | 137 | return result; 138 | } 139 | 140 | @Override 141 | public boolean acceptsURL(String url) { 142 | return url != null && url.startsWith(ACCEPTABLE_URL); 143 | } 144 | 145 | @Override 146 | public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) { 147 | return new DriverPropertyInfo[]{}; 148 | } 149 | 150 | @Override 151 | public int getMajorVersion() { 152 | return 1; 153 | } 154 | 155 | @Override 156 | public int getMinorVersion() { 157 | return 1; 158 | } 159 | 160 | @Override 161 | public boolean jdbcCompliant() { 162 | return false; 163 | } 164 | 165 | @Override 166 | public Logger getParentLogger() throws SQLFeatureNotSupportedException { 167 | throw new SQLFeatureNotSupportedException(); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/connection/ForceConnection.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.connection; 2 | 3 | import com.ascendix.jdbc.salesforce.statement.ForcePreparedStatement; 4 | import com.ascendix.jdbc.salesforce.metadata.ForceDatabaseMetaData; 5 | import com.sforce.soap.partner.PartnerConnection; 6 | 7 | import java.sql.Array; 8 | import java.sql.Blob; 9 | import java.sql.CallableStatement; 10 | import java.sql.Clob; 11 | import java.sql.Connection; 12 | import java.sql.DatabaseMetaData; 13 | import java.sql.NClob; 14 | import java.sql.PreparedStatement; 15 | import java.sql.SQLClientInfoException; 16 | import java.sql.SQLException; 17 | import java.sql.SQLWarning; 18 | import java.sql.SQLXML; 19 | import java.sql.Savepoint; 20 | import java.sql.Statement; 21 | import java.sql.Struct; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.Properties; 25 | import java.util.concurrent.Executor; 26 | import java.util.logging.Logger; 27 | 28 | public class ForceConnection implements Connection { 29 | 30 | private final PartnerConnection partnerConnection; 31 | private final DatabaseMetaData metadata; 32 | private static final String SF_JDBC_DRIVER_NAME = "SF JDBC driver"; 33 | private static final Logger logger = Logger.getLogger(SF_JDBC_DRIVER_NAME); 34 | 35 | private Map connectionCache = new HashMap<>(); 36 | Properties clientInfo = new Properties(); 37 | 38 | public ForceConnection(PartnerConnection partnerConnection) { 39 | this.partnerConnection = partnerConnection; 40 | this.metadata = new ForceDatabaseMetaData(this); 41 | } 42 | 43 | public PartnerConnection getPartnerConnection() { 44 | return partnerConnection; 45 | } 46 | 47 | public DatabaseMetaData getMetaData() { 48 | return metadata; 49 | } 50 | 51 | @Override 52 | public PreparedStatement prepareStatement(String soql) { 53 | return new ForcePreparedStatement(this, soql); 54 | } 55 | 56 | @Override 57 | public String getSchema() { 58 | return "Salesforce"; 59 | } 60 | 61 | public Map getCache() { 62 | return connectionCache; 63 | } 64 | 65 | @Override 66 | public T unwrap(Class iface) { 67 | // TODO Auto-generated method stub 68 | return null; 69 | } 70 | 71 | @Override 72 | public boolean isWrapperFor(Class iface) { 73 | // TODO Auto-generated method stub 74 | return false; 75 | } 76 | 77 | @Override 78 | public Statement createStatement() { 79 | logger.info("[Conn] createStatement 1 IMPLEMENTED "); 80 | return null; 81 | } 82 | 83 | @Override 84 | public CallableStatement prepareCall(String sql) { 85 | logger.info("[Conn] prepareCall NOT_IMPLEMENTED "+sql); 86 | return null; 87 | } 88 | 89 | @Override 90 | public String nativeSQL(String sql) { 91 | logger.info("[Conn] nativeSQL NOT_IMPLEMENTED "+sql); 92 | return null; 93 | } 94 | 95 | @Override 96 | public void setAutoCommit(boolean autoCommit) { 97 | // TODO Auto-generated method stub 98 | 99 | } 100 | 101 | @Override 102 | public boolean getAutoCommit() throws SQLException { 103 | // TODO Auto-generated method stub 104 | return false; 105 | } 106 | 107 | @Override 108 | public void commit() throws SQLException { 109 | // TODO Auto-generated method stub 110 | 111 | } 112 | 113 | @Override 114 | public void rollback() throws SQLException { 115 | // TODO Auto-generated method stub 116 | 117 | } 118 | 119 | @Override 120 | public void close() throws SQLException { 121 | // TODO Auto-generated method stub 122 | 123 | } 124 | 125 | @Override 126 | public boolean isClosed() throws SQLException { 127 | // TODO Auto-generated method stub 128 | return false; 129 | } 130 | 131 | @Override 132 | public void setReadOnly(boolean readOnly) throws SQLException { 133 | // TODO Auto-generated method stub 134 | 135 | } 136 | 137 | @Override 138 | public boolean isReadOnly() throws SQLException { 139 | // TODO Auto-generated method stub 140 | return false; 141 | } 142 | 143 | @Override 144 | public void setCatalog(String catalog) throws SQLException { 145 | // TODO Auto-generated method stub 146 | 147 | } 148 | 149 | @Override 150 | public String getCatalog() throws SQLException { 151 | // TODO Auto-generated method stub 152 | return null; 153 | } 154 | 155 | @Override 156 | public void setTransactionIsolation(int level) throws SQLException { 157 | // TODO Auto-generated method stub 158 | 159 | } 160 | 161 | @Override 162 | public int getTransactionIsolation() throws SQLException { 163 | // TODO Auto-generated method stub 164 | return 0; 165 | } 166 | 167 | @Override 168 | public SQLWarning getWarnings() throws SQLException { 169 | // TODO Auto-generated method stub 170 | return null; 171 | } 172 | 173 | @Override 174 | public void clearWarnings() throws SQLException { 175 | // TODO Auto-generated method stub 176 | 177 | } 178 | 179 | @Override 180 | public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { 181 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 182 | return null; 183 | } 184 | 185 | @Override 186 | public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) 187 | throws SQLException { 188 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 189 | return null; 190 | } 191 | 192 | @Override 193 | public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { 194 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 195 | return null; 196 | } 197 | 198 | @Override 199 | public Map> getTypeMap() throws SQLException { 200 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 201 | return null; 202 | } 203 | 204 | @Override 205 | public void setTypeMap(Map> map) throws SQLException { 206 | // TODO Auto-generated method stub 207 | 208 | } 209 | 210 | @Override 211 | public void setHoldability(int holdability) throws SQLException { 212 | // TODO Auto-generated method stub 213 | 214 | } 215 | 216 | @Override 217 | public int getHoldability() throws SQLException { 218 | // TODO Auto-generated method stub 219 | return 0; 220 | } 221 | 222 | @Override 223 | public Savepoint setSavepoint() throws SQLException { 224 | // TODO Auto-generated method stub 225 | return null; 226 | } 227 | 228 | @Override 229 | public Savepoint setSavepoint(String name) throws SQLException { 230 | // TODO Auto-generated method stub 231 | return null; 232 | } 233 | 234 | @Override 235 | public void rollback(Savepoint savepoint) throws SQLException { 236 | // TODO Auto-generated method stub 237 | 238 | } 239 | 240 | @Override 241 | public void releaseSavepoint(Savepoint savepoint) throws SQLException { 242 | // TODO Auto-generated method stub 243 | 244 | } 245 | 246 | @Override 247 | public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) 248 | throws SQLException { 249 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 250 | return null; 251 | } 252 | 253 | @Override 254 | public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, 255 | int resultSetHoldability) throws SQLException { 256 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 257 | return null; 258 | } 259 | 260 | @Override 261 | public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, 262 | int resultSetHoldability) throws SQLException { 263 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 264 | return null; 265 | } 266 | 267 | @Override 268 | public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { 269 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 270 | return null; 271 | } 272 | 273 | @Override 274 | public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { 275 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 276 | return null; 277 | } 278 | 279 | @Override 280 | public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { 281 | Logger.getLogger(SF_JDBC_DRIVER_NAME).info(Object.class.getEnclosingMethod().getName()); 282 | return null; 283 | } 284 | 285 | @Override 286 | public Clob createClob() throws SQLException { 287 | // TODO Auto-generated method stub 288 | return null; 289 | } 290 | 291 | @Override 292 | public Blob createBlob() throws SQLException { 293 | // TODO Auto-generated method stub 294 | return null; 295 | } 296 | 297 | @Override 298 | public NClob createNClob() throws SQLException { 299 | // TODO Auto-generated method stub 300 | return null; 301 | } 302 | 303 | @Override 304 | public SQLXML createSQLXML() throws SQLException { 305 | // TODO Auto-generated method stub 306 | return null; 307 | } 308 | 309 | @Override 310 | public boolean isValid(int timeout) throws SQLException { 311 | // TODO Auto-generated method stub 312 | return false; 313 | } 314 | 315 | @Override 316 | public void setClientInfo(String name, String value) throws SQLClientInfoException { 317 | // TODO Auto-generated method stub 318 | logger.info("[Conn] setClientInfo 1 IMPLEMENTED "+name+"="+value); 319 | clientInfo.setProperty(name, value); 320 | } 321 | 322 | @Override 323 | public void setClientInfo(Properties properties) throws SQLClientInfoException { 324 | logger.info("[Conn] setClientInfo 2 IMPLEMENTED properties<>"); 325 | properties.stringPropertyNames().forEach(propName -> clientInfo.setProperty(propName, properties.getProperty(propName))); 326 | } 327 | 328 | @Override 329 | public String getClientInfo(String name) throws SQLException { 330 | logger.info("[Conn] getClientInfo 1 IMPLEMENTED for '"+name+"'"); 331 | return clientInfo.getProperty(name); 332 | } 333 | 334 | @Override 335 | public Properties getClientInfo() throws SQLException { 336 | logger.info("[Conn] getClientInfo 2 IMPLEMENTED "); 337 | return clientInfo; 338 | } 339 | 340 | @Override 341 | public Array createArrayOf(String typeName, Object[] elements) throws SQLException { 342 | // TODO Auto-generated method stub 343 | return null; 344 | } 345 | 346 | @Override 347 | public Struct createStruct(String typeName, Object[] attributes) throws SQLException { 348 | // TODO Auto-generated method stub 349 | return null; 350 | } 351 | 352 | @Override 353 | public void setSchema(String schema) throws SQLException { 354 | // TODO Auto-generated method stub 355 | 356 | } 357 | 358 | @Override 359 | public void abort(Executor executor) throws SQLException { 360 | // TODO Auto-generated method stub 361 | 362 | } 363 | 364 | @Override 365 | public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { 366 | // TODO Auto-generated method stub 367 | 368 | } 369 | 370 | @Override 371 | public int getNetworkTimeout() throws SQLException { 372 | // TODO Auto-generated method stub 373 | return 0; 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/connection/ForceConnectionInfo.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.connection; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | @Data 7 | @NoArgsConstructor 8 | public class ForceConnectionInfo { 9 | 10 | private String userName; 11 | private String password; 12 | private String sessionId; 13 | private Boolean sandbox; 14 | private Boolean https = true; 15 | private String apiVersion = ForceService.DEFAULT_API_VERSION; 16 | private String loginDomain; 17 | private String clientName; 18 | } 19 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/connection/ForceService.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.connection; 2 | 3 | import com.ascendix.salesforce.oauth.ForceOAuthClient; 4 | import com.sforce.soap.partner.Connector; 5 | import com.sforce.soap.partner.PartnerConnection; 6 | import com.sforce.ws.ConnectionException; 7 | import com.sforce.ws.ConnectorConfig; 8 | import lombok.experimental.UtilityClass; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.io.FileUtils; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.mapdb.DB; 13 | import org.mapdb.DBMaker; 14 | import org.mapdb.HTreeMap; 15 | import org.mapdb.Serializer; 16 | 17 | import java.util.concurrent.TimeUnit; 18 | 19 | @UtilityClass 20 | @Slf4j 21 | public class ForceService { 22 | 23 | public static final String DEFAULT_LOGIN_DOMAIN = "login.salesforce.com"; 24 | private static final String SANDBOX_LOGIN_DOMAIN = "test.salesforce.com"; 25 | private static final long CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(10); 26 | private static final long READ_TIMEOUT = TimeUnit.SECONDS.toMillis(30); 27 | public static final String DEFAULT_API_VERSION = "50.0"; 28 | public static final int EXPIRE_AFTER_CREATE = 60; 29 | public static final int EXPIRE_STORE_SIZE = 16; 30 | 31 | 32 | private static final DB cacheDb = DBMaker.tempFileDB().closeOnJvmShutdown().make(); 33 | 34 | private static HTreeMap partnerUrlCache = cacheDb 35 | .hashMap("PartnerUrlCache", Serializer.STRING, Serializer.STRING) 36 | .expireAfterCreate(EXPIRE_AFTER_CREATE, TimeUnit.MINUTES) 37 | .expireStoreSize(EXPIRE_STORE_SIZE * FileUtils.ONE_MB) 38 | .create(); 39 | 40 | 41 | private static String getPartnerUrl(String accessToken, boolean sandbox) { 42 | return partnerUrlCache.computeIfAbsent(accessToken, s -> getPartnerUrlFromUserInfo(accessToken, sandbox)); 43 | } 44 | 45 | private static String getPartnerUrlFromUserInfo(String accessToken, boolean sandbox) { 46 | return new ForceOAuthClient(CONNECTION_TIMEOUT, READ_TIMEOUT).getUserInfo(accessToken, sandbox).getPartnerUrl(); 47 | } 48 | 49 | public static PartnerConnection createPartnerConnection(ForceConnectionInfo info) throws ConnectionException { 50 | return info.getSessionId() != null ? createConnectionBySessionId(info) : createConnectionByUserCredential(info); 51 | } 52 | 53 | private static PartnerConnection createConnectionBySessionId(ForceConnectionInfo info) throws ConnectionException { 54 | ConnectorConfig partnerConfig = new ConnectorConfig(); 55 | partnerConfig.setSessionId(info.getSessionId()); 56 | 57 | if (info.getSandbox() != null) { 58 | partnerConfig.setServiceEndpoint(ForceService.getPartnerUrl(info.getSessionId(), info.getSandbox())); 59 | return Connector.newConnection(partnerConfig); 60 | } 61 | 62 | try { 63 | partnerConfig.setServiceEndpoint(ForceService.getPartnerUrl(info.getSessionId(), false)); 64 | return Connector.newConnection(partnerConfig); 65 | } catch (RuntimeException re) { 66 | try { 67 | partnerConfig.setServiceEndpoint(ForceService.getPartnerUrl(info.getSessionId(), true)); 68 | return Connector.newConnection(partnerConfig); 69 | } catch (RuntimeException r) { 70 | throw new ConnectionException(r.getMessage()); 71 | } 72 | } 73 | } 74 | 75 | private static PartnerConnection createConnectionByUserCredential(ForceConnectionInfo info) 76 | throws ConnectionException { 77 | ConnectorConfig partnerConfig = new ConnectorConfig(); 78 | partnerConfig.setUsername(info.getUserName()); 79 | partnerConfig.setPassword(info.getPassword()); 80 | 81 | PartnerConnection connection; 82 | 83 | if (info.getSandbox() != null) { 84 | partnerConfig.setAuthEndpoint(buildAuthEndpoint(info)); 85 | connection = Connector.newConnection(partnerConfig); 86 | } else { 87 | try { 88 | info.setSandbox(false); 89 | partnerConfig.setAuthEndpoint(buildAuthEndpoint(info)); 90 | connection = Connector.newConnection(partnerConfig); 91 | } catch (ConnectionException ce) { 92 | info.setSandbox(true); 93 | partnerConfig.setAuthEndpoint(buildAuthEndpoint(info)); 94 | connection = Connector.newConnection(partnerConfig); 95 | } 96 | } 97 | if (connection != null && StringUtils.isNotBlank(info.getClientName())) { 98 | connection.setCallOptions(info.getClientName(), null); 99 | } 100 | return connection; 101 | } 102 | 103 | private static String buildAuthEndpoint(ForceConnectionInfo info) { 104 | String protocol = info.getHttps() ? "https" : "http"; 105 | String domain = info.getSandbox() ? SANDBOX_LOGIN_DOMAIN : info.getLoginDomain() != null ? info.getLoginDomain() : DEFAULT_LOGIN_DOMAIN; 106 | return String.format("%s://%s/services/Soap/u/%s", protocol, domain, info.getApiVersion()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/delegates/ForceResultField.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.delegates; 2 | 3 | public class ForceResultField { 4 | 5 | public static final String NESTED_RESULT_SET_FIELD_TYPE = "nestedResultSet"; 6 | 7 | private String entityType; 8 | private String name; 9 | private Object value; 10 | private String fieldType; 11 | 12 | public ForceResultField(String entityType, String fieldType, String name, Object value) { 13 | 14 | super(); 15 | this.entityType = entityType; 16 | this.name = name; 17 | this.value = value; 18 | this.fieldType = fieldType; 19 | } 20 | 21 | public String getEntityType() { 22 | return entityType; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public Object getValue() { 30 | return value; 31 | } 32 | 33 | public String getFullName() { 34 | return entityType != null ? entityType + "." + name : name; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "SfResultField [entityType=" + entityType + ", name=" + name + ", value=" + value + "]"; 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | final int prime = 31; 45 | int result = 1; 46 | result = prime * result + ((entityType == null) ? 0 : entityType.hashCode()); 47 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 48 | result = prime * result + ((value == null) ? 0 : value.hashCode()); 49 | return result; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object obj) { 54 | if (this == obj) 55 | return true; 56 | if (obj == null) 57 | return false; 58 | if (getClass() != obj.getClass()) 59 | return false; 60 | ForceResultField other = (ForceResultField) obj; 61 | if (entityType == null) { 62 | if (other.entityType != null) 63 | return false; 64 | } else if (!entityType.equals(other.entityType)) 65 | return false; 66 | if (name == null) { 67 | if (other.name != null) 68 | return false; 69 | } else if (!name.equals(other.name)) 70 | return false; 71 | if (value == null) { 72 | if (other.value != null) 73 | return false; 74 | } else if (!value.equals(other.value)) 75 | return false; 76 | return true; 77 | } 78 | 79 | public String getFieldType() { 80 | return fieldType; 81 | } 82 | 83 | public void setValue(Object value) { 84 | this.value = value; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/delegates/PartnerResultToCrtesianTable.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.delegates; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | @SuppressWarnings({"rawtypes", "unchecked"}) 11 | public class PartnerResultToCrtesianTable { 12 | 13 | private List schema; 14 | 15 | private PartnerResultToCrtesianTable(List schema) { 16 | this.schema = schema; 17 | } 18 | 19 | public static List expand(List list, List schema) { 20 | PartnerResultToCrtesianTable expander = new PartnerResultToCrtesianTable(schema); 21 | return expander.expandOn(list, 0, 0); 22 | } 23 | 24 | private List expandOn(List rows, int columnPosition, int schemaPosititon) { 25 | return rows.stream() 26 | .map(row -> expandRow(row, columnPosition, schemaPosititon)) 27 | .flatMap(Collection::stream) 28 | .collect(Collectors.toList()); 29 | } 30 | 31 | private List expandRow(List row, int columnPosition, int schemaPosititon) { 32 | List result = new ArrayList<>(); 33 | if (schemaPosititon > schema.size() - 1) { 34 | result.add(row); 35 | return result; 36 | } else if (schema.get(schemaPosititon) instanceof List) { 37 | int nestedListSize = ((List) schema.get(schemaPosititon)).size(); 38 | Object value = row.get(columnPosition); 39 | List nestedList = value instanceof List ? (List) value : Collections.emptyList(); 40 | if (nestedList.isEmpty()) { 41 | result.add(expandRow(row, Collections.nCopies(nestedListSize, null), columnPosition)); 42 | } else { 43 | nestedList.forEach(item -> result.add(expandRow(row, item, columnPosition))); 44 | } 45 | return expandOn(result, columnPosition + nestedListSize, schemaPosititon + 1); 46 | } else { 47 | result.add(row); 48 | return expandOn(result, columnPosition + 1, schemaPosititon + 1); 49 | } 50 | 51 | } 52 | 53 | private static List expandRow(List row, Object nestedItem, int position) { 54 | List nestedItemsToInsert = nestedItem instanceof List ? (List) nestedItem : Arrays.asList(nestedItem); 55 | List newRow = new ArrayList<>(row.subList(0, position)); 56 | newRow.addAll(nestedItemsToInsert); 57 | newRow.addAll(row.subList(position + 1, row.size())); 58 | return newRow; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/delegates/PartnerService.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.delegates; 2 | 3 | import com.ascendix.jdbc.salesforce.metadata.Column; 4 | import com.ascendix.jdbc.salesforce.metadata.Table; 5 | import com.ascendix.jdbc.salesforce.statement.FieldDef; 6 | import com.sforce.soap.partner.DescribeGlobalResult; 7 | import com.sforce.soap.partner.DescribeGlobalSObjectResult; 8 | import com.sforce.soap.partner.DescribeSObjectResult; 9 | import com.sforce.soap.partner.Field; 10 | import com.sforce.soap.partner.PartnerConnection; 11 | import com.sforce.soap.partner.QueryResult; 12 | import com.sforce.ws.ConnectionException; 13 | import com.sforce.ws.bind.XmlObject; 14 | import org.apache.commons.collections4.IteratorUtils; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.Collections; 19 | import java.util.Iterator; 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | public class PartnerService { 25 | 26 | private PartnerConnection partnerConnection; 27 | private List sObjectTypesCache; 28 | 29 | public PartnerService(PartnerConnection partnerConnection) { 30 | this.partnerConnection = partnerConnection; 31 | } 32 | 33 | public List getTables() { 34 | List sObjects = getSObjectsDescription(); 35 | return sObjects.stream() 36 | .map(this::convertToTable) 37 | .collect(Collectors.toList()); 38 | } 39 | 40 | public DescribeSObjectResult describeSObject(String sObjectType) throws ConnectionException { 41 | return partnerConnection.describeSObject(sObjectType); 42 | } 43 | 44 | private Table convertToTable(DescribeSObjectResult so) { 45 | List fields = Arrays.asList(so.getFields()); 46 | List columns = fields.stream() 47 | .map(this::convertToColumn) 48 | .collect(Collectors.toList()); 49 | return new Table(so.getName(), null, columns); 50 | } 51 | 52 | private Column convertToColumn(Field field) { 53 | try { 54 | Column column = new Column(field.getName(), getType(field)); 55 | column.setNillable(false); 56 | column.setCalculated(field.isCalculated() || field.isAutoNumber()); 57 | String[] referenceTos = field.getReferenceTo(); 58 | if (referenceTos != null) { 59 | for (String referenceTo : referenceTos) { 60 | if (getSObjectTypes().contains(referenceTo)) { 61 | column.setReferencedTable(referenceTo); 62 | column.setReferencedColumn("Id"); 63 | } 64 | } 65 | } 66 | return column; 67 | } catch (ConnectionException e) { 68 | throw new RuntimeException(e); 69 | } 70 | } 71 | 72 | private String getType(Field field) { 73 | String s = field.getType().toString(); 74 | if (s.startsWith("_")) { 75 | s = s.substring("_".length()); 76 | } 77 | return s.equalsIgnoreCase("double") ? "decimal" : s; 78 | } 79 | 80 | private List getSObjectTypes() throws ConnectionException { 81 | if (sObjectTypesCache == null) { 82 | DescribeGlobalSObjectResult[] sobs = partnerConnection.describeGlobal().getSobjects(); 83 | sObjectTypesCache = Arrays.stream(sobs) 84 | .map(DescribeGlobalSObjectResult::getName) 85 | .collect(Collectors.toList()); 86 | } 87 | return sObjectTypesCache; 88 | 89 | } 90 | 91 | private List getSObjectsDescription() { 92 | DescribeGlobalResult describeGlobals = describeGlobal(); 93 | List tableNames = Arrays.stream(describeGlobals.getSobjects()) 94 | .map(DescribeGlobalSObjectResult::getName) 95 | .collect(Collectors.toList()); 96 | List> tableNamesBatched = toBatches(tableNames, 100); 97 | return tableNamesBatched.stream() 98 | .flatMap(batch -> describeSObjects(batch).stream()) 99 | .collect(Collectors.toList()); 100 | } 101 | 102 | private DescribeGlobalResult describeGlobal() { 103 | try { 104 | return partnerConnection.describeGlobal(); 105 | } catch (ConnectionException e) { 106 | throw new RuntimeException(e); 107 | } 108 | } 109 | 110 | private List describeSObjects(List batch) { 111 | DescribeSObjectResult[] result; 112 | try { 113 | result = partnerConnection.describeSObjects(batch.toArray(new String[0])); 114 | return Arrays.asList(result); 115 | } catch (ConnectionException e) { 116 | throw new RuntimeException(e); 117 | } 118 | } 119 | 120 | private List> toBatches(List objects, int batchSize) { 121 | List> result = new ArrayList<>(); 122 | for (int fromIndex = 0; fromIndex < objects.size(); fromIndex += batchSize) { 123 | int toIndex = Math.min(fromIndex + batchSize, objects.size()); 124 | result.add(objects.subList(fromIndex, toIndex)); 125 | } 126 | return result; 127 | } 128 | 129 | public List query(String soql, List expectedSchema) throws ConnectionException { 130 | List resultRows = Collections.synchronizedList(new LinkedList<>()); 131 | QueryResult queryResult = null; 132 | do { 133 | queryResult = queryResult == null ? partnerConnection.query(soql) 134 | : partnerConnection.queryMore(queryResult.getQueryLocator()); 135 | resultRows.addAll(removeServiceInfo(Arrays.asList(queryResult.getRecords()))); 136 | } while (!queryResult.isDone()); 137 | 138 | return PartnerResultToCrtesianTable.expand(resultRows, expectedSchema); 139 | } 140 | 141 | private List removeServiceInfo(Iterator rows) { 142 | return removeServiceInfo(IteratorUtils.toList(rows)); 143 | } 144 | 145 | private List removeServiceInfo(List rows) { 146 | return rows.stream() 147 | .filter(this::isDataObjectType) 148 | .map(this::removeServiceInfo) 149 | .collect(Collectors.toList()); 150 | } 151 | 152 | private List removeServiceInfo(XmlObject row) { 153 | return IteratorUtils.toList(row.getChildren()).stream() 154 | .filter(this::isDataObjectType) 155 | .skip(1) // Removes duplicate Id from SF Partner API response 156 | // (https://developer.salesforce.com/forums/?id=906F00000008kciIAA) 157 | .map(field -> isNestedResultset(field) 158 | ? removeServiceInfo(field.getChildren()) 159 | : toForceResultField(field)) 160 | .collect(Collectors.toList()); 161 | } 162 | 163 | private ForceResultField toForceResultField(XmlObject field) { 164 | String fieldType = field.getXmlType() != null ? field.getXmlType().getLocalPart() : null; 165 | if ("sObject".equalsIgnoreCase(fieldType)) { 166 | List children = new ArrayList<>(); 167 | field.getChildren().forEachRemaining(children::add); 168 | field = children.get(2); 169 | } 170 | String name = field.getName().getLocalPart(); 171 | Object value = field.getValue(); 172 | return new ForceResultField(null, fieldType, name, value); 173 | } 174 | 175 | private boolean isNestedResultset(XmlObject object) { 176 | return object.getXmlType() != null && "QueryResult".equals(object.getXmlType().getLocalPart()); 177 | } 178 | 179 | private final static List SOAP_RESPONSE_SERVICE_OBJECT_TYPES = Arrays.asList("type", "done", "queryLocator", 180 | "size"); 181 | 182 | private boolean isDataObjectType(XmlObject object) { 183 | return !SOAP_RESPONSE_SERVICE_OBJECT_TYPES.contains(object.getName().getLocalPart()); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/metadata/Column.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.metadata; 2 | 3 | import java.io.Serializable; 4 | 5 | public class Column implements Serializable { 6 | 7 | private Table table; 8 | private String name; 9 | private String type; 10 | private String referencedTable; 11 | private String referencedColumn; 12 | 13 | private Integer length; 14 | private boolean nillable; 15 | private String comments; 16 | private boolean calculated; 17 | 18 | public Column(String name, String type) { 19 | this.name = name; 20 | this.type = type; 21 | } 22 | 23 | public Table getTable() { 24 | return table; 25 | } 26 | 27 | public void setTable(Table table) { 28 | this.table = table; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public String getType() { 36 | return type; 37 | } 38 | 39 | public String getReferencedTable() { 40 | return referencedTable; 41 | } 42 | 43 | public String getReferencedColumn() { 44 | return referencedColumn; 45 | } 46 | 47 | public Integer getLength() { 48 | return length; 49 | } 50 | 51 | public void setLength(Integer length) { 52 | this.length = length; 53 | } 54 | 55 | public boolean isNillable() { 56 | return nillable; 57 | } 58 | 59 | public void setNillable(boolean nillable) { 60 | this.nillable = nillable; 61 | } 62 | 63 | public void setReferencedTable(String referencedTable) { 64 | this.referencedTable = referencedTable; 65 | } 66 | 67 | public void setReferencedColumn(String referencedColumn) { 68 | this.referencedColumn = referencedColumn; 69 | } 70 | 71 | public String getComments() { 72 | return comments; 73 | } 74 | 75 | public void setComments(String comments) { 76 | this.comments = comments; 77 | } 78 | 79 | public boolean isCalculated() { 80 | return calculated; 81 | } 82 | 83 | public void setCalculated(boolean calculated) { 84 | this.calculated = calculated; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/metadata/ColumnMap.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.metadata; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | 6 | public class ColumnMap implements Serializable { 7 | 8 | private static final long serialVersionUID = 2705233366870541749L; 9 | 10 | private ArrayList columnNames = new ArrayList<>(); 11 | private ArrayList values = new ArrayList<>(); 12 | private int columnPosition = 0; 13 | 14 | public V put(K key, V value) { 15 | columnNames.add(columnPosition, key); 16 | values.add(columnPosition, value); 17 | columnPosition++; 18 | return value; 19 | } 20 | 21 | public V get(K key) { 22 | int index = columnNames.indexOf(key); 23 | return index != -1 ? values.get(index) : null; 24 | } 25 | 26 | /** 27 | * Get a column name by index, starting at 1, that represents the insertion 28 | * order into the map. 29 | */ 30 | public V getByIndex(int index) { 31 | return values.get(index - 1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/metadata/Table.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.metadata; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | public class Table implements Serializable { 7 | 8 | private String name; 9 | private String comments; 10 | private List columns; 11 | 12 | public Table(String name, String comments, List columns) { 13 | this.name = name; 14 | this.comments = comments; 15 | this.columns = columns; 16 | for (Column c : columns) { 17 | c.setTable(this); 18 | } 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public String getComments() { 26 | return comments; 27 | } 28 | 29 | public List getColumns() { 30 | return columns; 31 | } 32 | 33 | public Column findColumn(String columnName) { 34 | return columns.stream() 35 | .filter(column -> columnName.equals(column.getName())) 36 | .findFirst() 37 | .orElse(null); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/resultset/CachedResultSet.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.resultset; 2 | 3 | import com.ascendix.jdbc.salesforce.metadata.ColumnMap; 4 | 5 | import javax.sql.rowset.serial.SerialBlob; 6 | import java.io.InputStream; 7 | import java.io.Reader; 8 | import java.io.Serializable; 9 | import java.math.BigDecimal; 10 | import java.net.URL; 11 | import java.sql.Array; 12 | import java.sql.Blob; 13 | import java.sql.Clob; 14 | import java.sql.Date; 15 | import java.sql.NClob; 16 | import java.sql.Ref; 17 | import java.sql.ResultSet; 18 | import java.sql.ResultSetMetaData; 19 | import java.sql.RowId; 20 | import java.sql.SQLException; 21 | import java.sql.SQLWarning; 22 | import java.sql.SQLXML; 23 | import java.sql.Statement; 24 | import java.sql.Time; 25 | import java.sql.Timestamp; 26 | import java.text.ParseException; 27 | import java.text.SimpleDateFormat; 28 | import java.util.Arrays; 29 | import java.util.Base64; 30 | import java.util.Calendar; 31 | import java.util.GregorianCalendar; 32 | import java.util.List; 33 | import java.util.Map; 34 | import java.util.Optional; 35 | import java.util.function.Function; 36 | 37 | public class CachedResultSet implements ResultSet, Serializable { 38 | 39 | private static final long serialVersionUID = 1L; 40 | 41 | private transient Integer index; 42 | private List> rows; 43 | private ResultSetMetaData metadata; 44 | 45 | public CachedResultSet(List> rows) { 46 | this.rows = rows; 47 | } 48 | 49 | public CachedResultSet(List> rows, ResultSetMetaData metadata) { 50 | this(rows); 51 | this.metadata = metadata; 52 | } 53 | 54 | public CachedResultSet(ColumnMap singleRow) { 55 | this(Arrays.asList(singleRow)); 56 | } 57 | 58 | public Object getObject(String columnName) throws SQLException { 59 | return rows.get(getIndex()).get(columnName.toUpperCase()); 60 | } 61 | 62 | public Object getObject(int columnIndex) throws SQLException { 63 | return rows.get(getIndex()).getByIndex(columnIndex); 64 | } 65 | 66 | private int getIndex() { 67 | if (index == null) { 68 | index = -1; 69 | } 70 | return index; 71 | } 72 | 73 | private void setIndex(int i) { 74 | index = i; 75 | } 76 | 77 | private void increaseIndex() { 78 | index = getIndex() + 1; 79 | } 80 | 81 | public String getString(String columnName) throws SQLException { 82 | return (String) getObject(columnName); 83 | } 84 | 85 | public String getString(int columnIndex) throws SQLException { 86 | return (String) getObject(columnIndex); 87 | } 88 | 89 | public boolean first() throws SQLException { 90 | if (rows.size() > 0) { 91 | setIndex(0); 92 | return true; 93 | } else { 94 | return false; 95 | } 96 | } 97 | 98 | public boolean last() throws SQLException { 99 | if (rows.size() > 0) { 100 | setIndex(rows.size() - 1); 101 | return true; 102 | } else { 103 | return false; 104 | } 105 | } 106 | 107 | public boolean next() throws SQLException { 108 | if (rows.size() > 0) { 109 | increaseIndex(); 110 | return getIndex() < rows.size(); 111 | } else { 112 | return false; 113 | } 114 | } 115 | 116 | public boolean isAfterLast() throws SQLException { 117 | return rows.size() > 0 && getIndex() == rows.size(); 118 | } 119 | 120 | public boolean isBeforeFirst() throws SQLException { 121 | return rows.size() > 0 && getIndex() == -1; 122 | } 123 | 124 | public boolean isFirst() throws SQLException { 125 | return rows.size() > 0 && getIndex() == 0; 126 | } 127 | 128 | public boolean isLast() throws SQLException { 129 | return rows.size() > 0 && getIndex() == rows.size() - 1; 130 | } 131 | 132 | public ResultSetMetaData getMetaData() throws SQLException { 133 | return metadata != null ? metadata : new CachedResultSetMetaData(); 134 | } 135 | 136 | public void setFetchSize(int rows) throws SQLException { 137 | } 138 | 139 | public Date getDate(int columnIndex, Calendar cal) throws SQLException { 140 | throw new UnsupportedOperationException("Not implemented yet."); 141 | } 142 | 143 | public Date getDate(String columnName, Calendar cal) throws SQLException { 144 | throw new UnsupportedOperationException("Not implemented yet."); 145 | } 146 | 147 | private class ColumnValueParser { 148 | 149 | private Function conversion; 150 | 151 | public ColumnValueParser(Function parser) { 152 | this.conversion = parser; 153 | } 154 | 155 | public Optional parse(int columnIndex) { 156 | Object value = rows.get(getIndex()).getByIndex(columnIndex); 157 | return parse(value); 158 | } 159 | 160 | public Optional parse(String columnName) { 161 | Object value = rows.get(getIndex()).get(columnName.toUpperCase()); 162 | return parse(value); 163 | } 164 | 165 | private Optional parse(Object o) { 166 | if (o == null) return Optional.empty(); 167 | if (!(o instanceof String)) return (Optional) Optional.of(o); 168 | return Optional.of(conversion.apply((String) o)); 169 | } 170 | 171 | } 172 | 173 | public BigDecimal getBigDecimal(int columnIndex) throws SQLException { 174 | return new ColumnValueParser<>(BigDecimal::new) 175 | .parse(columnIndex) 176 | .orElse(null); 177 | } 178 | 179 | public BigDecimal getBigDecimal(String columnName) throws SQLException { 180 | return new ColumnValueParser<>(BigDecimal::new) 181 | .parse(columnName) 182 | .orElse(null); 183 | } 184 | 185 | protected java.util.Date parseDate(String dateRepr) { 186 | try { 187 | return new SimpleDateFormat("yyyy-MM-dd").parse(dateRepr); 188 | } catch (ParseException e) { 189 | throw new RuntimeException(e); 190 | } 191 | } 192 | 193 | public Date getDate(int columnIndex) throws SQLException { 194 | return new ColumnValueParser<>(this::parseDate) 195 | .parse(columnIndex) 196 | .map(d -> new java.sql.Date(d.getTime())) 197 | .orElse(null); 198 | } 199 | 200 | public Date getDate(String columnName) throws SQLException { 201 | return new ColumnValueParser<>(this::parseDate) 202 | .parse(columnName) 203 | .map(d -> new java.sql.Date(d.getTime())) 204 | .orElse(null); 205 | } 206 | 207 | private java.util.Date parseDateTime(String dateRepr) { 208 | try { 209 | return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX").parse(dateRepr); 210 | } catch (ParseException e) { 211 | throw new RuntimeException(e); 212 | } 213 | } 214 | 215 | public Timestamp getTimestamp(int columnIndex) throws SQLException { 216 | Object value = rows.get(getIndex()).getByIndex(columnIndex); 217 | if (value instanceof GregorianCalendar) { 218 | return new java.sql.Timestamp(((GregorianCalendar) value).getTime().getTime()); 219 | } else { 220 | return new ColumnValueParser<>(this::parseDateTime) 221 | .parse(columnIndex) 222 | .map(d -> new java.sql.Timestamp(d.getTime())) 223 | .orElse(null); 224 | } 225 | } 226 | 227 | public Timestamp getTimestamp(String columnName) throws SQLException { 228 | Object value = rows.get(getIndex()).get(columnName); 229 | if (value instanceof GregorianCalendar) { 230 | return new java.sql.Timestamp(((GregorianCalendar) value).getTime().getTime()); 231 | } else { 232 | return new ColumnValueParser<>((v) -> parseDateTime(v)) 233 | .parse(columnName) 234 | .map(d -> new java.sql.Timestamp(d.getTime())) 235 | .orElse(null); 236 | } 237 | } 238 | 239 | public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { 240 | throw new UnsupportedOperationException("Not implemented yet."); 241 | } 242 | 243 | public Timestamp getTimestamp(String columnName, Calendar cal) throws SQLException { 244 | throw new UnsupportedOperationException("Not implemented yet."); 245 | } 246 | 247 | private java.util.Date parseTime(String dateRepr) { 248 | try { 249 | return new SimpleDateFormat("HH:mm:ss.SSSX").parse(dateRepr); 250 | } catch (ParseException e) { 251 | throw new RuntimeException(e); 252 | } 253 | } 254 | 255 | public Time getTime(String columnName) throws SQLException { 256 | return new ColumnValueParser<>(this::parseTime) 257 | .parse(columnName) 258 | .map(d -> new Time(d.getTime())) 259 | .orElse(null); 260 | } 261 | 262 | public Time getTime(int columnIndex) throws SQLException { 263 | return new ColumnValueParser<>(this::parseTime) 264 | .parse(columnIndex) 265 | .map(d -> new Time(d.getTime())) 266 | .orElse(null); 267 | } 268 | 269 | public BigDecimal getBigDecimal(int columnIndex, int scale) { 270 | Optional result = new ColumnValueParser<>(BigDecimal::new) 271 | .parse(columnIndex); 272 | result.ifPresent(v -> v.setScale(scale)); 273 | return result.orElse(null); 274 | } 275 | 276 | public BigDecimal getBigDecimal(String columnName, int scale) { 277 | Optional result = new ColumnValueParser<>(BigDecimal::new) 278 | .parse(columnName); 279 | result.ifPresent(v -> v.setScale(scale)); 280 | return result.orElse(null); 281 | } 282 | 283 | public float getFloat(int columnIndex) throws SQLException { 284 | return new ColumnValueParser<>(Float::new) 285 | .parse(columnIndex) 286 | .orElse(0f); 287 | } 288 | 289 | public float getFloat(String columnName) throws SQLException { 290 | return new ColumnValueParser<>(Float::new) 291 | .parse(columnName) 292 | .orElse(0f); 293 | } 294 | 295 | public double getDouble(int columnIndex) throws SQLException { 296 | return new ColumnValueParser<>(Double::new) 297 | .parse(columnIndex) 298 | .orElse(0d); 299 | } 300 | 301 | public double getDouble(String columnName) throws SQLException { 302 | return new ColumnValueParser<>(Double::new) 303 | .parse(columnName) 304 | .orElse(0d); 305 | } 306 | 307 | public long getLong(String columnName) throws SQLException { 308 | return new ColumnValueParser<>(Long::new) 309 | .parse(columnName) 310 | .orElse(0L); 311 | } 312 | 313 | public long getLong(int columnIndex) throws SQLException { 314 | return new ColumnValueParser(Long::new) 315 | .parse(columnIndex) 316 | .orElse(0L); 317 | } 318 | 319 | public int getInt(String columnName) throws SQLException { 320 | return new ColumnValueParser<>(Integer::new) 321 | .parse(columnName) 322 | .orElse(0); 323 | } 324 | 325 | public int getInt(int columnIndex) throws SQLException { 326 | return new ColumnValueParser<>(Integer::new) 327 | .parse(columnIndex) 328 | .orElse(0); 329 | } 330 | 331 | public short getShort(String columnName) throws SQLException { 332 | return new ColumnValueParser<>(Short::new) 333 | .parse(columnName) 334 | .orElse((short) 0); 335 | } 336 | 337 | public short getShort(int columnIndex) throws SQLException { 338 | return new ColumnValueParser<>(Short::new) 339 | .parse(columnIndex) 340 | .orElse((short) 0); 341 | } 342 | 343 | public InputStream getBinaryStream(int columnIndex) throws SQLException { 344 | throw new UnsupportedOperationException("Not implemented yet."); 345 | } 346 | 347 | public InputStream getBinaryStream(String columnName) throws SQLException { 348 | throw new UnsupportedOperationException("Not implemented yet."); 349 | } 350 | 351 | private Blob createBlob(byte[] data) { 352 | try { 353 | return new SerialBlob(data); 354 | } catch (SQLException e) { 355 | throw new RuntimeException(e); 356 | } 357 | } 358 | 359 | public Blob getBlob(int columnIndex) throws SQLException { 360 | return new ColumnValueParser<>((v) -> Base64.getDecoder().decode(v)) 361 | .parse(columnIndex) 362 | .map(this::createBlob) 363 | .orElse(null); 364 | } 365 | 366 | public Blob getBlob(String columnName) throws SQLException { 367 | return new ColumnValueParser<>((v) -> Base64.getDecoder().decode(v)) 368 | .parse(columnName) 369 | .map(this::createBlob) 370 | .orElse(null); 371 | } 372 | 373 | public boolean getBoolean(int columnIndex) throws SQLException { 374 | return new ColumnValueParser<>(Boolean::new) 375 | .parse(columnIndex) 376 | .orElse(false); 377 | } 378 | 379 | public boolean getBoolean(String columnName) throws SQLException { 380 | return new ColumnValueParser<>(Boolean::new) 381 | .parse(columnName) 382 | .orElse(false); 383 | } 384 | 385 | public byte getByte(int columnIndex) throws SQLException { 386 | return new ColumnValueParser<>(Byte::new) 387 | .parse(columnIndex) 388 | .orElse((byte) 0); 389 | } 390 | 391 | public byte getByte(String columnName) throws SQLException { 392 | return new ColumnValueParser<>(Byte::new) 393 | .parse(columnName) 394 | .orElse((byte) 0); 395 | } 396 | 397 | public byte[] getBytes(int columnIndex) throws SQLException { 398 | throw new UnsupportedOperationException("Not implemented yet."); 399 | } 400 | 401 | public byte[] getBytes(String columnName) throws SQLException { 402 | throw new UnsupportedOperationException("Not implemented yet."); 403 | } 404 | 405 | // 406 | // Not implemented below here 407 | // 408 | 409 | public boolean absolute(int row) throws SQLException { 410 | return false; 411 | } 412 | 413 | public void afterLast() throws SQLException { 414 | System.out.println("after last check"); 415 | } 416 | 417 | public void beforeFirst() throws SQLException { 418 | } 419 | 420 | public void cancelRowUpdates() throws SQLException { 421 | } 422 | 423 | public void clearWarnings() throws SQLException { 424 | 425 | } 426 | 427 | public void close() throws SQLException { 428 | 429 | } 430 | 431 | public void deleteRow() throws SQLException { 432 | 433 | } 434 | 435 | public int findColumn(String columnName) throws SQLException { 436 | 437 | return 0; 438 | } 439 | 440 | public Array getArray(int i) throws SQLException { 441 | 442 | return null; 443 | } 444 | 445 | public Array getArray(String colName) throws SQLException { 446 | 447 | return null; 448 | } 449 | 450 | public InputStream getAsciiStream(int columnIndex) throws SQLException { 451 | 452 | return null; 453 | } 454 | 455 | public InputStream getAsciiStream(String columnName) throws SQLException { 456 | 457 | return null; 458 | } 459 | 460 | public Reader getCharacterStream(int columnIndex) throws SQLException { 461 | 462 | return null; 463 | } 464 | 465 | public Reader getCharacterStream(String columnName) throws SQLException { 466 | 467 | return null; 468 | } 469 | 470 | public Clob getClob(int i) throws SQLException { 471 | 472 | return null; 473 | } 474 | 475 | public Clob getClob(String colName) throws SQLException { 476 | 477 | return null; 478 | } 479 | 480 | public int getConcurrency() throws SQLException { 481 | 482 | return 0; 483 | } 484 | 485 | public String getCursorName() throws SQLException { 486 | 487 | return null; 488 | } 489 | 490 | public int getFetchDirection() throws SQLException { 491 | 492 | return 0; 493 | } 494 | 495 | public int getFetchSize() throws SQLException { 496 | 497 | return 0; 498 | } 499 | 500 | public Object getObject(int i, Map> map) 501 | throws SQLException { 502 | 503 | return null; 504 | } 505 | 506 | public Object getObject(String colName, Map> map) 507 | throws SQLException { 508 | 509 | return null; 510 | } 511 | 512 | public Ref getRef(int i) throws SQLException { 513 | 514 | return null; 515 | } 516 | 517 | public Ref getRef(String colName) throws SQLException { 518 | 519 | return null; 520 | } 521 | 522 | public int getRow() throws SQLException { 523 | 524 | return 0; 525 | } 526 | 527 | public Statement getStatement() throws SQLException { 528 | 529 | return null; 530 | } 531 | 532 | public Time getTime(int columnIndex, Calendar cal) throws SQLException { 533 | 534 | return null; 535 | } 536 | 537 | public Time getTime(String columnName, Calendar cal) throws SQLException { 538 | 539 | return null; 540 | } 541 | 542 | public int getType() throws SQLException { 543 | 544 | return 0; 545 | } 546 | 547 | public URL getURL(int columnIndex) throws SQLException { 548 | 549 | return null; 550 | } 551 | 552 | public URL getURL(String columnName) throws SQLException { 553 | 554 | return null; 555 | } 556 | 557 | public InputStream getUnicodeStream(int columnIndex) throws SQLException { 558 | 559 | return null; 560 | } 561 | 562 | public InputStream getUnicodeStream(String columnName) throws SQLException { 563 | 564 | return null; 565 | } 566 | 567 | public SQLWarning getWarnings() throws SQLException { 568 | 569 | return null; 570 | } 571 | 572 | public void insertRow() throws SQLException { 573 | } 574 | 575 | public void moveToCurrentRow() throws SQLException { 576 | } 577 | 578 | public void moveToInsertRow() throws SQLException { 579 | } 580 | 581 | public boolean previous() throws SQLException { 582 | 583 | return false; 584 | } 585 | 586 | public void refreshRow() throws SQLException { 587 | } 588 | 589 | public boolean relative(int rows) throws SQLException { 590 | 591 | return false; 592 | } 593 | 594 | public boolean rowDeleted() throws SQLException { 595 | 596 | return false; 597 | } 598 | 599 | public boolean rowInserted() throws SQLException { 600 | 601 | return false; 602 | } 603 | 604 | public boolean rowUpdated() throws SQLException { 605 | 606 | return false; 607 | } 608 | 609 | public void setFetchDirection(int direction) throws SQLException { 610 | } 611 | 612 | public void updateArray(int columnIndex, Array x) throws SQLException { 613 | } 614 | 615 | public void updateArray(String columnName, Array x) throws SQLException { 616 | } 617 | 618 | public void updateAsciiStream(int columnIndex, InputStream x, int length) 619 | throws SQLException { 620 | } 621 | 622 | public void updateAsciiStream(String columnName, InputStream x, int length) 623 | throws SQLException { 624 | } 625 | 626 | public void updateBigDecimal(int columnIndex, BigDecimal x) 627 | throws SQLException { 628 | } 629 | 630 | public void updateBigDecimal(String columnName, BigDecimal x) 631 | throws SQLException { 632 | } 633 | 634 | public void updateBinaryStream(int columnIndex, InputStream x, int length) 635 | throws SQLException { 636 | } 637 | 638 | public void updateBinaryStream(String columnName, InputStream x, int length) 639 | throws SQLException { 640 | } 641 | 642 | public void updateBlob(int columnIndex, Blob x) throws SQLException { 643 | } 644 | 645 | public void updateBlob(String columnName, Blob x) throws SQLException { 646 | } 647 | 648 | public void updateBoolean(int columnIndex, boolean x) throws SQLException { 649 | } 650 | 651 | public void updateBoolean(String columnName, boolean x) throws SQLException { 652 | } 653 | 654 | public void updateByte(int columnIndex, byte x) throws SQLException { 655 | } 656 | 657 | public void updateByte(String columnName, byte x) throws SQLException { 658 | } 659 | 660 | public void updateBytes(int columnIndex, byte[] x) throws SQLException { 661 | } 662 | 663 | public void updateBytes(String columnName, byte[] x) throws SQLException { 664 | } 665 | 666 | public void updateCharacterStream(int columnIndex, Reader x, int length) 667 | throws SQLException { 668 | } 669 | 670 | public void updateCharacterStream(String columnName, Reader reader, 671 | int length) throws SQLException { 672 | } 673 | 674 | public void updateClob(int columnIndex, Clob x) throws SQLException { 675 | } 676 | 677 | public void updateClob(String columnName, Clob x) throws SQLException { 678 | } 679 | 680 | public void updateDate(int columnIndex, Date x) throws SQLException { 681 | } 682 | 683 | public void updateDate(String columnName, Date x) throws SQLException { 684 | } 685 | 686 | public void updateDouble(int columnIndex, double x) throws SQLException { 687 | } 688 | 689 | public void updateDouble(String columnName, double x) throws SQLException { 690 | } 691 | 692 | public void updateFloat(int columnIndex, float x) throws SQLException { 693 | } 694 | 695 | public void updateFloat(String columnName, float x) throws SQLException { 696 | } 697 | 698 | public void updateInt(int columnIndex, int x) throws SQLException { 699 | } 700 | 701 | public void updateInt(String columnName, int x) throws SQLException { 702 | } 703 | 704 | public void updateLong(int columnIndex, long x) throws SQLException { 705 | } 706 | 707 | public void updateLong(String columnName, long x) throws SQLException { 708 | } 709 | 710 | public void updateNull(int columnIndex) throws SQLException { 711 | } 712 | 713 | public void updateNull(String columnName) throws SQLException { 714 | } 715 | 716 | public void updateObject(int columnIndex, Object x) throws SQLException { 717 | } 718 | 719 | public void updateObject(String columnName, Object x) throws SQLException { 720 | } 721 | 722 | public void updateObject(int columnIndex, Object x, int scale) 723 | throws SQLException { 724 | } 725 | 726 | public void updateObject(String columnName, Object x, int scale) 727 | throws SQLException { 728 | } 729 | 730 | public void updateRef(int columnIndex, Ref x) throws SQLException { 731 | } 732 | 733 | public void updateRef(String columnName, Ref x) throws SQLException { 734 | } 735 | 736 | public void updateRow() throws SQLException { 737 | } 738 | 739 | public void updateShort(int columnIndex, short x) throws SQLException { 740 | } 741 | 742 | public void updateShort(String columnName, short x) throws SQLException { 743 | } 744 | 745 | public void updateString(int columnIndex, String x) throws SQLException { 746 | } 747 | 748 | public void updateString(String columnName, String x) throws SQLException { 749 | } 750 | 751 | public void updateTime(int columnIndex, Time x) throws SQLException { 752 | } 753 | 754 | public void updateTime(String columnName, Time x) throws SQLException { 755 | } 756 | 757 | public void updateTimestamp(int columnIndex, Timestamp x) 758 | throws SQLException { 759 | } 760 | 761 | public void updateTimestamp(String columnName, Timestamp x) 762 | throws SQLException { 763 | } 764 | 765 | public boolean wasNull() throws SQLException { 766 | 767 | return false; 768 | } 769 | 770 | public T unwrap(Class iface) throws SQLException { 771 | 772 | return null; 773 | } 774 | 775 | public boolean isWrapperFor(Class iface) throws SQLException { 776 | 777 | return false; 778 | } 779 | 780 | public RowId getRowId(int columnIndex) throws SQLException { 781 | 782 | return null; 783 | } 784 | 785 | public RowId getRowId(String columnLabel) throws SQLException { 786 | 787 | return null; 788 | } 789 | 790 | public void updateRowId(int columnIndex, RowId x) throws SQLException { 791 | } 792 | 793 | public void updateRowId(String columnLabel, RowId x) throws SQLException { 794 | } 795 | 796 | public int getHoldability() throws SQLException { 797 | 798 | return 0; 799 | } 800 | 801 | public boolean isClosed() throws SQLException { 802 | 803 | return false; 804 | } 805 | 806 | public void updateNString(int columnIndex, String nString) throws SQLException { 807 | } 808 | 809 | public void updateNString(String columnLabel, String nString) throws SQLException { 810 | } 811 | 812 | public void updateNClob(int columnIndex, NClob nClob) throws SQLException { 813 | } 814 | 815 | public void updateNClob(String columnLabel, NClob nClob) throws SQLException { 816 | } 817 | 818 | public NClob getNClob(int columnIndex) throws SQLException { 819 | 820 | return null; 821 | } 822 | 823 | public NClob getNClob(String columnLabel) throws SQLException { 824 | 825 | return null; 826 | } 827 | 828 | public SQLXML getSQLXML(int columnIndex) throws SQLException { 829 | 830 | return null; 831 | } 832 | 833 | public SQLXML getSQLXML(String columnLabel) throws SQLException { 834 | 835 | return null; 836 | } 837 | 838 | public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { 839 | } 840 | 841 | public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { 842 | } 843 | 844 | public String getNString(int columnIndex) throws SQLException { 845 | 846 | return null; 847 | } 848 | 849 | public String getNString(String columnLabel) throws SQLException { 850 | 851 | return null; 852 | } 853 | 854 | public Reader getNCharacterStream(int columnIndex) throws SQLException { 855 | 856 | return null; 857 | } 858 | 859 | public Reader getNCharacterStream(String columnLabel) throws SQLException { 860 | 861 | return null; 862 | } 863 | 864 | public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { 865 | } 866 | 867 | public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { 868 | } 869 | 870 | public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { 871 | } 872 | 873 | public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { 874 | } 875 | 876 | public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { 877 | } 878 | 879 | public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { 880 | } 881 | 882 | public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { 883 | } 884 | 885 | public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { 886 | } 887 | 888 | public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { 889 | } 890 | 891 | public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { 892 | } 893 | 894 | public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { 895 | } 896 | 897 | public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { 898 | } 899 | 900 | public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { 901 | } 902 | 903 | public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { 904 | } 905 | 906 | public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { 907 | } 908 | 909 | public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { 910 | } 911 | 912 | public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { 913 | } 914 | 915 | public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { 916 | } 917 | 918 | public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { 919 | } 920 | 921 | public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { 922 | } 923 | 924 | public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { 925 | } 926 | 927 | public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { 928 | } 929 | 930 | public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { 931 | } 932 | 933 | public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { 934 | } 935 | 936 | public void updateClob(int columnIndex, Reader reader) throws SQLException { 937 | } 938 | 939 | public void updateClob(String columnLabel, Reader reader) throws SQLException { 940 | } 941 | 942 | public void updateNClob(int columnIndex, Reader reader) throws SQLException { 943 | } 944 | 945 | public void updateNClob(String columnLabel, Reader reader) throws SQLException { 946 | } 947 | 948 | @Override 949 | public T getObject(int columnIndex, Class type) throws SQLException { 950 | // TODO Auto-generated method stub 951 | return null; 952 | } 953 | 954 | @Override 955 | public T getObject(String columnLabel, Class type) throws SQLException { 956 | // TODO Auto-generated method stub 957 | return null; 958 | } 959 | } 960 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/resultset/CachedResultSetMetaData.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.resultset; 2 | 3 | import java.sql.ResultSetMetaData; 4 | import java.sql.SQLException; 5 | 6 | public class CachedResultSetMetaData implements ResultSetMetaData { 7 | 8 | public String getCatalogName(int column) throws SQLException { 9 | return ""; 10 | } 11 | 12 | // 13 | // Not implemented below here 14 | // 15 | 16 | public String getColumnClassName(int column) throws SQLException { 17 | 18 | return null; 19 | } 20 | 21 | public int getColumnCount() throws SQLException { 22 | 23 | return 0; 24 | } 25 | 26 | public int getColumnDisplaySize(int column) throws SQLException { 27 | 28 | return 0; 29 | } 30 | 31 | public String getColumnLabel(int column) throws SQLException { 32 | 33 | return null; 34 | } 35 | 36 | public String getColumnName(int column) throws SQLException { 37 | 38 | return null; 39 | } 40 | 41 | public int getColumnType(int column) throws SQLException { 42 | 43 | return 0; 44 | } 45 | 46 | public String getColumnTypeName(int column) throws SQLException { 47 | 48 | return null; 49 | } 50 | 51 | public int getPrecision(int column) throws SQLException { 52 | 53 | return 0; 54 | } 55 | 56 | public int getScale(int column) throws SQLException { 57 | 58 | return 0; 59 | } 60 | 61 | public String getSchemaName(int column) throws SQLException { 62 | 63 | return null; 64 | } 65 | 66 | public String getTableName(int column) throws SQLException { 67 | 68 | return null; 69 | } 70 | 71 | public boolean isAutoIncrement(int column) throws SQLException { 72 | 73 | return false; 74 | } 75 | 76 | public boolean isCaseSensitive(int column) throws SQLException { 77 | 78 | return false; 79 | } 80 | 81 | public boolean isCurrency(int column) throws SQLException { 82 | 83 | return false; 84 | } 85 | 86 | public boolean isDefinitelyWritable(int column) throws SQLException { 87 | 88 | return false; 89 | } 90 | 91 | public int isNullable(int column) throws SQLException { 92 | 93 | return 0; 94 | } 95 | 96 | public boolean isReadOnly(int column) throws SQLException { 97 | 98 | return false; 99 | } 100 | 101 | public boolean isSearchable(int column) throws SQLException { 102 | 103 | return false; 104 | } 105 | 106 | public boolean isSigned(int column) throws SQLException { 107 | 108 | return false; 109 | } 110 | 111 | public boolean isWritable(int column) throws SQLException { 112 | 113 | return false; 114 | } 115 | 116 | public T unwrap(Class iface) throws SQLException { 117 | 118 | return null; 119 | } 120 | 121 | public boolean isWrapperFor(Class iface) throws SQLException { 122 | 123 | return false; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/statement/FieldDef.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.statement; 2 | 3 | public class FieldDef { 4 | 5 | private String name; 6 | private String type; 7 | 8 | public FieldDef(String name, String type) { 9 | this.name = name; 10 | this.type = type; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public String getType() { 18 | return type; 19 | } 20 | 21 | @Override 22 | public int hashCode() { 23 | final int prime = 31; 24 | int result = 1; 25 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 26 | result = prime * result + ((type == null) ? 0 : type.hashCode()); 27 | return result; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object obj) { 32 | if (this == obj) 33 | return true; 34 | if (obj == null) 35 | return false; 36 | if (getClass() != obj.getClass()) 37 | return false; 38 | FieldDef other = (FieldDef) obj; 39 | if (name == null) { 40 | if (other.name != null) 41 | return false; 42 | } else if (!name.equals(other.name)) 43 | return false; 44 | if (type == null) { 45 | if (other.type != null) 46 | return false; 47 | } else if (!type.equals(other.type)) 48 | return false; 49 | return true; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/statement/ParameterMetadataImpl.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.statement; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import java.sql.ParameterMetaData; 6 | import java.sql.SQLException; 7 | import java.sql.Types; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class ParameterMetadataImpl implements ParameterMetaData { 13 | 14 | private List parameters = new ArrayList<>(); 15 | 16 | public ParameterMetadataImpl(List parameters, String query) { 17 | super(); 18 | this.parameters.addAll(parameters); 19 | int paramsCountInQuery = StringUtils.countMatches(query, '?'); 20 | if (this.parameters.size() < paramsCountInQuery) { 21 | this.parameters.addAll(Collections.nCopies(paramsCountInQuery - this.parameters.size(), new Object())); 22 | } 23 | } 24 | 25 | @Override 26 | public T unwrap(Class iface) throws SQLException { 27 | // TODO Auto-generated method stub 28 | return null; 29 | } 30 | 31 | @Override 32 | public boolean isWrapperFor(Class iface) throws SQLException { 33 | // TODO Auto-generated method stub 34 | return false; 35 | } 36 | 37 | @Override 38 | public int getParameterCount() throws SQLException { 39 | return parameters.size(); 40 | } 41 | 42 | @Override 43 | public int isNullable(int param) throws SQLException { 44 | return ParameterMetaData.parameterNullable; 45 | } 46 | 47 | @Override 48 | public boolean isSigned(int param) throws SQLException { 49 | return parameters.get(param + 1).getClass().isInstance(Number.class); 50 | } 51 | 52 | @Override 53 | public int getPrecision(int param) throws SQLException { 54 | return 0; 55 | } 56 | 57 | @Override 58 | public int getScale(int param) throws SQLException { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public int getParameterType(int param) throws SQLException { 64 | return Types.NVARCHAR; 65 | } 66 | 67 | @Override 68 | public String getParameterTypeName(int param) throws SQLException { 69 | return "varchar"; 70 | } 71 | 72 | @Override 73 | public String getParameterClassName(int param) throws SQLException { 74 | return parameters.get(param + 1).getClass().getName(); 75 | } 76 | 77 | @Override 78 | public int getParameterMode(int param) throws SQLException { 79 | return ParameterMetaData.parameterModeIn; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/main/java/com/ascendix/jdbc/salesforce/statement/SoqlQueryAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.statement; 2 | 3 | import com.sforce.soap.partner.ChildRelationship; 4 | import com.sforce.soap.partner.DescribeSObjectResult; 5 | import com.sforce.soap.partner.Field; 6 | import org.mule.tools.soql.SOQLDataBaseVisitor; 7 | import org.mule.tools.soql.SOQLParserHelper; 8 | import org.mule.tools.soql.query.SOQLQuery; 9 | import org.mule.tools.soql.query.SOQLSubQuery; 10 | import org.mule.tools.soql.query.clause.FromClause; 11 | import org.mule.tools.soql.query.from.ObjectSpec; 12 | import org.mule.tools.soql.query.select.FieldSpec; 13 | import org.mule.tools.soql.query.select.FunctionCallSpec; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.function.Function; 21 | 22 | public class SoqlQueryAnalyzer { 23 | 24 | private String soql; 25 | private Function objectDescriptor; 26 | private Map describedObjectsCache; 27 | private SOQLQuery queryData; 28 | 29 | public SoqlQueryAnalyzer(String soql, Function objectDescriptor) { 30 | this(soql, objectDescriptor, new HashMap<>()); 31 | } 32 | 33 | public SoqlQueryAnalyzer(String soql, Function objectDescriptor, Map describedObjectsCache) { 34 | this.soql = soql; 35 | this.objectDescriptor = objectDescriptor; 36 | this.describedObjectsCache = describedObjectsCache; 37 | } 38 | 39 | private List fieldDefinitions; 40 | 41 | private class SelectSpecVisitor extends SOQLDataBaseVisitor { 42 | 43 | @Override 44 | public Void visitFieldSpec(FieldSpec fieldSpec) { 45 | String name = fieldSpec.getFieldName(); 46 | String alias = fieldSpec.getAlias() != null ? fieldSpec.getAlias() : name; 47 | List prefixNames = new ArrayList<>(fieldSpec.getObjectPrefixNames()); 48 | FieldDef result = createFieldDef(name, alias, prefixNames); 49 | fieldDefinitions.add(result); 50 | return null; 51 | } 52 | 53 | private FieldDef createFieldDef(String name, String alias, List prefixNames) { 54 | List fieldPrefixes = new ArrayList<>(prefixNames); 55 | String fromObject = getFromObjectName(); 56 | if (!fieldPrefixes.isEmpty() && fieldPrefixes.get(0).equalsIgnoreCase(fromObject)) { 57 | fieldPrefixes.remove(0); 58 | } 59 | while (!fieldPrefixes.isEmpty()) { 60 | String referenceName = fieldPrefixes.get(0); 61 | Field reference = findField(referenceName, describeObject(fromObject), fld -> fld.getRelationshipName()); 62 | fromObject = reference.getReferenceTo()[0]; 63 | fieldPrefixes.remove(0); 64 | } 65 | String type = findField(name, describeObject(fromObject), fld -> fld.getName()).getType().name(); 66 | FieldDef result = new FieldDef(alias, type); 67 | return result; 68 | } 69 | 70 | private final List FUNCTIONS_HAS_INT_RESULT = Arrays.asList("COUNT", "COUNT_DISTINCT", "CALENDAR_MONTH", 71 | "CALENDAR_QUARTER", "CALENDAR_YEAR", "DAY_IN_MONTH", "DAY_IN_WEEK", "DAY_IN_YEAR", "DAY_ONLY", "FISCAL_MONTH", 72 | "FISCAL_QUARTER", "FISCAL_YEAR", "HOUR_IN_DAY", "WEEK_IN_MONTH", "WEEK_IN_YEAR"); 73 | 74 | @Override 75 | public Void visitFunctionCallSpec(FunctionCallSpec functionCallSpec) { 76 | String alias = functionCallSpec.getAlias() != null ? functionCallSpec.getAlias() : functionCallSpec.getFunctionName(); 77 | if (FUNCTIONS_HAS_INT_RESULT.contains(functionCallSpec.getFunctionName().toUpperCase())) { 78 | fieldDefinitions.add(new FieldDef(alias, "int")); 79 | } else { 80 | org.mule.tools.soql.query.data.Field param = (org.mule.tools.soql.query.data.Field) functionCallSpec.getFunctionParameters().get(0); 81 | FieldDef result = createFieldDef(param.getFieldName(), alias, param.getObjectPrefixNames()); 82 | fieldDefinitions.add(result); 83 | } 84 | return null; 85 | } 86 | 87 | @Override 88 | public Void visitSOQLSubQuery(SOQLSubQuery soqlSubQuery) { 89 | String subquerySoql = soqlSubQuery.toSOQLText().replaceAll("\\A\\s*\\(|\\)\\s*$", ""); 90 | SOQLQuery subquery = SOQLParserHelper.createSOQLData(subquerySoql); 91 | String relationshipName = subquery.getFromClause().getMainObjectSpec().getObjectName(); 92 | ChildRelationship relatedFrom = Arrays.stream(describeObject(getFromObjectName()).getChildRelationships()) 93 | .filter(rel -> relationshipName.equalsIgnoreCase(rel.getRelationshipName())) 94 | .findFirst() 95 | .orElseThrow(() -> new IllegalArgumentException("Unresolved relationship in subquery \"" + subquerySoql + "\"")); 96 | String fromObject = relatedFrom.getChildSObject(); 97 | subquery.setFromClause(new FromClause(new ObjectSpec(fromObject, null))); 98 | 99 | SoqlQueryAnalyzer subqueryAnalyzer = new SoqlQueryAnalyzer(subquery.toSOQLText(), objectDescriptor, describedObjectsCache); 100 | fieldDefinitions.add(new ArrayList(subqueryAnalyzer.getFieldDefinitions())); 101 | return null; 102 | } 103 | 104 | } 105 | 106 | public List getFieldDefinitions() { 107 | if (fieldDefinitions == null) { 108 | fieldDefinitions = new ArrayList<>(); 109 | SelectSpecVisitor visitor = new SelectSpecVisitor(); 110 | getQueryData().getSelectSpecs() 111 | .forEach(spec -> spec.accept(visitor)); 112 | } 113 | return fieldDefinitions; 114 | } 115 | 116 | private Field findField(String name, DescribeSObjectResult objectDesc, Function nameFetcher) { 117 | return Arrays.stream(objectDesc.getFields()) 118 | .filter(field -> name.equals(nameFetcher.apply(field))) 119 | .findFirst() 120 | .orElseThrow(() -> new IllegalArgumentException("Unknown field name \"" + name + "\" in object \"" + objectDesc.getName() + "\"")); 121 | } 122 | 123 | private DescribeSObjectResult describeObject(String fromObjectName) { 124 | if (!describedObjectsCache.containsKey(fromObjectName)) { 125 | DescribeSObjectResult description = objectDescriptor.apply(fromObjectName); 126 | describedObjectsCache.put(fromObjectName, description); 127 | return description; 128 | } else { 129 | return describedObjectsCache.get(fromObjectName); 130 | } 131 | } 132 | 133 | protected String getFromObjectName() { 134 | return getQueryData().getFromClause().getMainObjectSpec().getObjectName(); 135 | } 136 | 137 | private SOQLQuery getQueryData() { 138 | if (queryData == null) { 139 | queryData = SOQLParserHelper.createSOQLData(soql); 140 | } 141 | return queryData; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/com/ascendix/jdbc/salesforce/ForceDriverTest.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.io.IOException; 7 | import java.sql.Connection; 8 | import java.sql.SQLException; 9 | import java.util.Properties; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class ForceDriverTest { 14 | 15 | private ForceDriver driver; 16 | 17 | @Before 18 | public void setUp() { 19 | driver = new ForceDriver(); 20 | } 21 | 22 | @Test 23 | public void testGetConnStringProperties() throws IOException { 24 | Properties actuals = driver.getConnStringProperties("jdbc:ascendix:salesforce://prop1=val1;prop2=val2"); 25 | 26 | assertEquals(2, actuals.size()); 27 | assertEquals("val1", actuals.getProperty("prop1")); 28 | assertEquals("val2", actuals.getProperty("prop2")); 29 | } 30 | 31 | @Test 32 | public void testGetConnStringProperties_WhenNoValue() throws IOException { 33 | Properties actuals = driver.getConnStringProperties("jdbc:ascendix:salesforce://prop1=val1; prop2; prop3 = val3"); 34 | 35 | assertEquals(3, actuals.size()); 36 | assertTrue(actuals.containsKey("prop2")); 37 | assertEquals("", actuals.getProperty("prop2")); 38 | } 39 | 40 | @Test 41 | public void testConnect_WhenWrongURL() throws SQLException { 42 | Connection connection = driver.connect("jdbc:mysql://localhost/test", new Properties()); 43 | 44 | assertNull(connection); 45 | } 46 | 47 | @Test 48 | public void testGetConnStringProperties_StandartUrlFormat() throws IOException { 49 | Properties actuals = driver.getConnStringProperties("jdbc:ascendix:salesforce://test@test.ru:aaaa!aaa@login.salesforce.ru"); 50 | 51 | assertEquals(3, actuals.size()); 52 | assertTrue(actuals.containsKey("user")); 53 | assertEquals("test@test.ru", actuals.getProperty("user")); 54 | assertEquals("aaaa!aaa", actuals.getProperty("password")); 55 | assertEquals("login.salesforce.ru", actuals.getProperty("loginDomain")); 56 | } 57 | 58 | @Test 59 | public void testGetConnStringProperties_StandartUrlFormatHttpsApi() throws IOException { 60 | Properties actuals = driver.getConnStringProperties("jdbc:ascendix:salesforce://test@test.ru:aaaa!aaa@login.salesforce.ru?https=false&api=48.0"); 61 | 62 | assertEquals(5, actuals.size()); 63 | assertTrue(actuals.containsKey("user")); 64 | assertEquals("test@test.ru", actuals.getProperty("user")); 65 | assertEquals("aaaa!aaa", actuals.getProperty("password")); 66 | assertEquals("login.salesforce.ru", actuals.getProperty("loginDomain")); 67 | assertEquals("false", actuals.getProperty("https")); 68 | assertEquals("48.0", actuals.getProperty("api")); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/com/ascendix/jdbc/salesforce/delegates/PartnerResultToCrtesianTableTest.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.delegates; 2 | 3 | import com.ascendix.jdbc.salesforce.delegates.PartnerResultToCrtesianTable; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | 14 | public class PartnerResultToCrtesianTableTest { 15 | 16 | @Test 17 | public void testExpandSimple() { 18 | List schema = Arrays.asList(new Object(), new Object(), new Object(), new Object()); 19 | 20 | List expected = Arrays.asList( 21 | (List) Arrays.asList(1, 2, 3, 4) 22 | ); 23 | 24 | List actual = PartnerResultToCrtesianTable.expand(expected, schema); 25 | 26 | assertEquals(actual, expected); 27 | } 28 | 29 | @Test 30 | public void testExpandWhenNothingToExpand() { 31 | List schema = Arrays.asList(new Object(), new Object(), new Object(), new Object()); 32 | 33 | List expected = Arrays.asList( 34 | Arrays.asList(1, 2, 3, 4), 35 | Arrays.asList("1", "2", "3", "4"), 36 | Arrays.asList("11", "12", "13", "14"), 37 | Arrays.asList("21", "22", "23", "24") 38 | ); 39 | 40 | List actual = PartnerResultToCrtesianTable.expand(expected, schema); 41 | 42 | assertEquals(actual, expected); 43 | } 44 | 45 | @Test 46 | public void testExpandWhenOneNestedList() { 47 | List schema = Arrays.asList(new Object(), Arrays.asList(new Object(), new Object(), new Object()), new Object(), new Object()); 48 | 49 | List list = Arrays.asList( 50 | (List) Arrays.asList("1", Arrays.asList("21", "22", "23"), "3", "4") 51 | ); 52 | 53 | List expected = Arrays.asList( 54 | Arrays.asList("1", "21", "3", "4"), 55 | Arrays.asList("1", "22", "3", "4"), 56 | Arrays.asList("1", "23", "3", "4") 57 | ); 58 | 59 | List actual = PartnerResultToCrtesianTable.expand(list, schema); 60 | 61 | assertEquals(actual, expected); 62 | } 63 | 64 | @Test 65 | public void testExpandWhenTwoNestedListAndOneRow() { 66 | List schema = Arrays.asList(new Object(), Arrays.asList(new Object(), new Object()), new Object(), Arrays.asList(new Object(), new Object())); 67 | 68 | List list = Arrays.asList( 69 | (List) Arrays.asList(11, Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4)), 12, Arrays.asList(Arrays.asList(5, 6), Arrays.asList(7, 8))) 70 | ); 71 | 72 | List expected = Arrays.asList( 73 | Arrays.asList(11, 1, 2, 12, 5, 6), 74 | Arrays.asList(11, 3, 4, 12, 5, 6), 75 | Arrays.asList(11, 1, 2, 12, 7, 8), 76 | Arrays.asList(11, 3, 4, 12, 7, 8) 77 | ); 78 | 79 | List actual = PartnerResultToCrtesianTable.expand(list, schema); 80 | 81 | assertEquals(expected.size(), actual.size()); 82 | for (List l : expected) { 83 | assertTrue(actual.contains(l)); 84 | } 85 | } 86 | 87 | @Test 88 | public void testExpandWhenOneNestedListAndTwoRows() { 89 | List schema = Arrays.asList(new Object(), Arrays.asList(new Object(), new Object()), new Object(), new Object()); 90 | 91 | List list = Arrays.asList( 92 | Arrays.asList(11, Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4)), 12, 13), 93 | Arrays.asList(20, Arrays.asList(Arrays.asList(21, 22), Arrays.asList(23, 24), Arrays.asList(25, 26)), 41, 42) 94 | ); 95 | 96 | List expected = Arrays.asList( 97 | Arrays.asList(11, 1, 2, 12, 13), 98 | Arrays.asList(11, 3, 4, 12, 13), 99 | Arrays.asList(20, 21, 22, 41, 42), 100 | Arrays.asList(20, 23, 24, 41, 42), 101 | Arrays.asList(20, 25, 26, 41, 42) 102 | ); 103 | 104 | List actual = PartnerResultToCrtesianTable.expand(list, schema); 105 | 106 | assertEquals(actual, expected); 107 | } 108 | 109 | @Test 110 | public void testExpandWhenOneNestedListIsEmpty() { 111 | List schema = Arrays.asList(new Object(), Arrays.asList(new Object(), new Object()), new Object(), new Object()); 112 | 113 | List list = Arrays.asList( 114 | (List) Arrays.asList(11, new ArrayList(), 12, 13) 115 | ); 116 | 117 | List expected = Arrays.asList( 118 | (List) Arrays.asList(11, null, null, 12, 13) 119 | ); 120 | 121 | List actual = PartnerResultToCrtesianTable.expand(list, schema); 122 | 123 | assertEquals(actual, expected); 124 | } 125 | 126 | @Test 127 | public void testExpandWhenNestedListIsEmpty() { 128 | List schema = Arrays.asList(new Object(), Arrays.asList(new Object(), new Object())); 129 | 130 | List list = Arrays.asList( 131 | (List) Arrays.asList(11, new Object()) 132 | ); 133 | 134 | List expected = Arrays.asList( 135 | (List) Arrays.asList(11, null, null) 136 | ); 137 | 138 | List actual = PartnerResultToCrtesianTable.expand(list, schema); 139 | 140 | assertEquals(actual, expected); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/com/ascendix/jdbc/salesforce/metadata/ForceDatabaseMetaDataTest.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.metadata; 2 | 3 | import com.ascendix.jdbc.salesforce.metadata.ForceDatabaseMetaData; 4 | import org.junit.Test; 5 | 6 | import java.sql.Types; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | public class ForceDatabaseMetaDataTest { 11 | 12 | @Test 13 | public void testLookupTypeInfo() { 14 | ForceDatabaseMetaData.TypeInfo actual = ForceDatabaseMetaData.lookupTypeInfo("int"); 15 | 16 | assertEquals("int", actual.typeName); 17 | assertEquals(Types.INTEGER, actual.sqlDataType); 18 | } 19 | 20 | @Test 21 | public void testLookupTypeInfo_IfTypeUnknown() { 22 | ForceDatabaseMetaData.TypeInfo actual = ForceDatabaseMetaData.lookupTypeInfo("my strange type"); 23 | 24 | assertEquals("other", actual.typeName); 25 | assertEquals(Types.OTHER, actual.sqlDataType); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/com/ascendix/jdbc/salesforce/resultset/CachedResultSetTest.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.resultset; 2 | 3 | import com.ascendix.jdbc.salesforce.metadata.ColumnMap; 4 | import com.ascendix.jdbc.salesforce.resultset.CachedResultSet; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.util.Calendar; 9 | import java.util.Date; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class CachedResultSetTest { 14 | 15 | private CachedResultSet cachedResultSet; 16 | 17 | @Before 18 | public void setUp() { 19 | ColumnMap columnMap = new ColumnMap<>(); 20 | cachedResultSet = new CachedResultSet(columnMap); 21 | } 22 | 23 | @Test 24 | public void testParseDate() { 25 | Date actual = cachedResultSet.parseDate("2017-06-23"); 26 | 27 | Calendar calendar = Calendar.getInstance(); 28 | calendar.setTime(actual); 29 | 30 | assertEquals(2017, calendar.get(Calendar.YEAR)); 31 | assertEquals(Calendar.JUNE, calendar.get(Calendar.MONTH)); 32 | assertEquals(23, calendar.get(Calendar.DAY_OF_MONTH)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/com/ascendix/jdbc/salesforce/statement/ForcePreparedStatementTest.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.statement; 2 | 3 | import com.ascendix.jdbc.salesforce.statement.ForcePreparedStatement; 4 | import org.junit.Test; 5 | 6 | import java.math.BigDecimal; 7 | import java.text.SimpleDateFormat; 8 | import java.util.GregorianCalendar; 9 | import java.util.List; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertNull; 13 | 14 | public class ForcePreparedStatementTest { 15 | 16 | @Test 17 | public void testGetParamClass() { 18 | assertEquals(String.class, ForcePreparedStatement.getParamClass("test")); 19 | assertEquals(Long.class, ForcePreparedStatement.getParamClass(1L)); 20 | assertEquals(Object.class, ForcePreparedStatement.getParamClass(new SimpleDateFormat())); 21 | assertNull(ForcePreparedStatement.getParamClass(null)); 22 | } 23 | 24 | @Test 25 | public void testToSoqlStringParam() { 26 | assertEquals("'\\''", ForcePreparedStatement.toSoqlStringParam("'")); 27 | assertEquals("'\\\\'", ForcePreparedStatement.toSoqlStringParam("\\")); 28 | assertEquals("'\\';DELETE DATABASE \\\\a'", ForcePreparedStatement.toSoqlStringParam("';DELETE DATABASE \\a")); 29 | } 30 | 31 | @Test 32 | public void testConvertToSoqlParam() { 33 | assertEquals("123.45", ForcePreparedStatement.convertToSoqlParam(123.45)); 34 | assertEquals("123.45", ForcePreparedStatement.convertToSoqlParam(123.45f)); 35 | assertEquals("123", ForcePreparedStatement.convertToSoqlParam(123L)); 36 | assertEquals("123.45", ForcePreparedStatement.convertToSoqlParam(new BigDecimal("123.45"))); 37 | assertEquals("2017-03-06T12:34:56", ForcePreparedStatement.convertToSoqlParam(new GregorianCalendar(2017, 2, 6, 12, 34, 56).getTime())); 38 | assertEquals("'\\'test\\'\\\\'", ForcePreparedStatement.convertToSoqlParam("'test'\\")); 39 | assertEquals("NULL", ForcePreparedStatement.convertToSoqlParam(null)); 40 | } 41 | 42 | @Test 43 | public void testAddParameter() { 44 | ForcePreparedStatement statement = new ForcePreparedStatement(null, ""); 45 | statement.addParameter(1, "one"); 46 | statement.addParameter(3, "two"); 47 | 48 | List actuals = statement.getParameters(); 49 | 50 | assertEquals(3, actuals.size()); 51 | assertEquals("one", actuals.get(0)); 52 | assertEquals("two", actuals.get(2)); 53 | assertNull(actuals.get(1)); 54 | } 55 | 56 | @Test 57 | public void testSetParams() { 58 | ForcePreparedStatement statement = new ForcePreparedStatement(null, ""); 59 | String query = "SELECT Something FROM Anything WERE name = ? AND age > ?"; 60 | statement.addParameter(1, "one"); 61 | statement.addParameter(2, 123); 62 | 63 | String actual = statement.setParams(query); 64 | 65 | assertEquals("SELECT Something FROM Anything WERE name = 'one' AND age > 123", actual); 66 | } 67 | 68 | 69 | @Test 70 | public void testGetCacheMode() { 71 | ForcePreparedStatement statement = new ForcePreparedStatement(null, ""); 72 | 73 | assertEquals(ForcePreparedStatement.CacheMode.SESSION, statement.getCacheMode("CACHE SESSION select name from Account")); 74 | assertEquals(ForcePreparedStatement.CacheMode.GLOBAL, statement.getCacheMode(" Cache global select name from Account")); 75 | assertEquals(ForcePreparedStatement.CacheMode.NO_CACHE, statement.getCacheMode("select name from Account")); 76 | assertEquals(ForcePreparedStatement.CacheMode.NO_CACHE, statement.getCacheMode(" Cache unknown select name from Account")); 77 | } 78 | 79 | @Test 80 | public void removeCacheHints() { 81 | ForcePreparedStatement statement = new ForcePreparedStatement(null, ""); 82 | assertEquals(" select name from Account", statement.removeCacheHints(" Cache global select name from Account")); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/com/ascendix/jdbc/salesforce/statement/SoqlQueryAnalyzerTest.java: -------------------------------------------------------------------------------- 1 | package com.ascendix.jdbc.salesforce.statement; 2 | 3 | import com.sforce.soap.partner.DescribeSObjectResult; 4 | import com.thoughtworks.xstream.XStream; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | 16 | public class SoqlQueryAnalyzerTest { 17 | 18 | @Test 19 | public void testGetFieldNames_SimpleQuery() { 20 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer(" select Id ,Name \r\nfrom Account\r\n where something = 'nothing' ", n -> this.describeSObject(n)); 21 | List expecteds = Arrays.asList("Id", "Name"); 22 | List actuals = listFlatFieldNames(analyzer); 23 | 24 | assertEquals(expecteds, actuals); 25 | } 26 | 27 | private List listFlatFieldNames(SoqlQueryAnalyzer analyzer) { 28 | return listFlatFieldDefinitions(analyzer.getFieldDefinitions()).stream() 29 | .map(FieldDef::getName) 30 | .collect(Collectors.toList()); 31 | } 32 | 33 | @Test 34 | public void testGetFieldNames_SelectWithReferences() { 35 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer(" select Id , Account.Name \r\nfrom Contact\r\n where something = 'nothing' ", n -> this.describeSObject(n)); 36 | List expecteds = Arrays.asList("Id", "Name"); 37 | List actuals = listFlatFieldNames(analyzer); 38 | 39 | assertEquals(expecteds, actuals); 40 | } 41 | 42 | @Test 43 | public void testGetFieldNames_SelectWithAggregateAliased() { 44 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer(" select Id , Account.Name, count(id) aggrAlias1\r\nfrom Contact\r\n where something = 'nothing' ", n -> this.describeSObject(n)); 45 | List expecteds = Arrays.asList("Id", "Name", "aggrAlias1"); 46 | List actuals = listFlatFieldNames(analyzer); 47 | 48 | assertEquals(expecteds, actuals); 49 | } 50 | 51 | @Test 52 | public void testGetFieldNames_SelectWithAggregate() { 53 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer(" select Id , Account.Name, count(id)\r\nfrom Contact\r\n where something = 'nothing' ", n -> this.describeSObject(n)); 54 | List expecteds = Arrays.asList("Id", "Name", "count"); 55 | List actuals = listFlatFieldNames(analyzer); 56 | 57 | assertEquals(expecteds, actuals); 58 | } 59 | 60 | @Test 61 | public void testGetFromObjectName() { 62 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer(" select Id , Account.Name \r\nfrom Contact\r\n where something = 'nothing' ", n -> this.describeSObject(n)); 63 | String expected = "Contact"; 64 | String actual = analyzer.getFromObjectName(); 65 | 66 | assertEquals(expected, actual); 67 | } 68 | 69 | private List listFlatFieldDefinitions(List fieldDefs) { 70 | return (List) fieldDefs.stream() 71 | .flatMap(def -> def instanceof List 72 | ? ((List) def).stream() 73 | : Arrays.asList(def).stream()) 74 | .collect(Collectors.toList()); 75 | } 76 | 77 | @Test 78 | public void testGetSimpleFieldDefinitions() { 79 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT Id, Name FROM Account", n -> this.describeSObject(n)); 80 | 81 | List actuals = listFlatFieldDefinitions(analyzer.getFieldDefinitions()); 82 | assertEquals(2, actuals.size()); 83 | assertEquals("Id", actuals.get(0).getName()); 84 | assertEquals("id", actuals.get(0).getType()); 85 | 86 | assertEquals("Name", actuals.get(1).getName()); 87 | assertEquals("string", actuals.get(1).getType()); 88 | } 89 | 90 | @Test 91 | public void testGetReferenceFieldDefinitions() { 92 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT Account.Name FROM Contact", n -> this.describeSObject(n)); 93 | 94 | List actuals = listFlatFieldDefinitions(analyzer.getFieldDefinitions()); 95 | assertEquals(1, actuals.size()); 96 | assertEquals("Name", actuals.get(0).getName()); 97 | assertEquals("string", actuals.get(0).getType()); 98 | } 99 | 100 | @Test 101 | public void testGetAggregateFieldDefinition() { 102 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT MIN(Name) FROM Contact", n -> this.describeSObject(n)); 103 | 104 | List actuals = listFlatFieldDefinitions(analyzer.getFieldDefinitions()); 105 | assertEquals(1, actuals.size()); 106 | assertEquals("MIN", actuals.get(0).getName()); 107 | assertEquals("string", actuals.get(0).getType()); 108 | } 109 | 110 | @Test 111 | public void testGetAggregateFieldDefinitionWithoutParameter() { 112 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT Count() FROM Contact", n -> this.describeSObject(n)); 113 | 114 | List actuals = listFlatFieldDefinitions(analyzer.getFieldDefinitions()); 115 | assertEquals(1, actuals.size()); 116 | assertEquals("Count", actuals.get(0).getName()); 117 | assertEquals("int", actuals.get(0).getType()); 118 | } 119 | 120 | @Test 121 | public void testGetSimpleFieldWithQualifier() { 122 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT Contact.Id FROM Contact", n -> this.describeSObject(n)); 123 | 124 | List actuals = listFlatFieldDefinitions(analyzer.getFieldDefinitions()); 125 | assertEquals(1, actuals.size()); 126 | assertEquals("Id", actuals.get(0).getName()); 127 | assertEquals("id", actuals.get(0).getType()); 128 | } 129 | 130 | @Test 131 | public void testGetNamedAggregateFieldDefinitions() { 132 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT count(Name) nameCount FROM Account", n -> this.describeSObject(n)); 133 | 134 | List actuals = listFlatFieldDefinitions(analyzer.getFieldDefinitions()); 135 | 136 | assertEquals(1, actuals.size()); 137 | assertEquals("nameCount", actuals.get(0).getName()); 138 | assertEquals("int", actuals.get(0).getType()); 139 | } 140 | 141 | private DescribeSObjectResult describeSObject(String sObjectType) { 142 | try { 143 | String xml = new String(Files.readAllBytes(Paths.get("src/test/resources/" + sObjectType + "_desription.xml"))); 144 | XStream xstream = new XStream(); 145 | return (DescribeSObjectResult) xstream.fromXML(xml); 146 | } catch (IOException e) { 147 | throw new RuntimeException(e); 148 | } 149 | } 150 | 151 | @Test 152 | public void testFetchFieldDefinitions_WithIncludedSeslect() { 153 | SoqlQueryAnalyzer analyzer = new SoqlQueryAnalyzer("SELECT Name, (SELECT Id, max(LastName) maxLastName FROM Contacts), Id FROM Account", n -> this.describeSObject(n)); 154 | 155 | List actuals = analyzer.getFieldDefinitions(); 156 | 157 | assertEquals(3, actuals.size()); 158 | FieldDef fieldDef = (FieldDef) actuals.get(0); 159 | assertEquals("Name", fieldDef.getName()); 160 | assertEquals("string", fieldDef.getType()); 161 | 162 | List suqueryDef = (List) actuals.get(1); 163 | fieldDef = (FieldDef) suqueryDef.get(0); 164 | assertEquals("Id", fieldDef.getName()); 165 | assertEquals("id", fieldDef.getType()); 166 | 167 | fieldDef = (FieldDef) suqueryDef.get(1); 168 | assertEquals("maxLastName", fieldDef.getName()); 169 | assertEquals("string", fieldDef.getType()); 170 | 171 | fieldDef = (FieldDef) actuals.get(2); 172 | assertEquals("Id", fieldDef.getName()); 173 | assertEquals("id", fieldDef.getType()); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /sf-jdbc-driver/src/test/java/util/DBTablePrinter.java: -------------------------------------------------------------------------------- 1 | /* 2 | Database Table Printer 3 | Copyright (C) 2014 Hami Galip Torun 4 | 5 | Email: hamitorun@e-fabrika.net 6 | Project Home: https://github.com/htorun/dbtableprinter 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | /* 23 | This is my first Java program that does something more or less 24 | useful. It is part of my effort to learn Java, how to use 25 | an IDE (IntelliJ IDEA 13.1.15 in this case), how to apply an 26 | open source license and how to use Git and GitHub (https://github.com) 27 | for version control and publishing an open source software. 28 | 29 | Hami 30 | */ 31 | 32 | package net.efabrika.util; 33 | 34 | import java.sql.Connection; 35 | import java.sql.ResultSet; 36 | import java.sql.ResultSetMetaData; 37 | import java.sql.SQLException; 38 | import java.sql.Statement; 39 | import java.sql.Types; 40 | import java.util.ArrayList; 41 | import java.util.List; 42 | import java.util.StringJoiner; 43 | 44 | /** 45 | * Just a utility to print rows from a given DB table or a 46 | * ResultSet to standard out, formatted to look 47 | * like a table with rows and columns with borders. 48 | * 49 | *

Stack Overflow website 50 | * (stackoverflow.com) 51 | * was the primary source of inspiration and help to put this 52 | * code together. Especially the questions and answers of 53 | * the following people were very useful:

54 | * 55 | *

Question: 56 | * How to display or 57 | * print the contents of a database table as is
58 | * People: sky scraper

59 | * 60 | *

Question: 61 | * System.out.println() 62 | * from database into a table
63 | * People: Simon Cottrill, Tony Toews, Costis Aivali, Riggy, corsiKa

64 | * 65 | *

Question: 66 | * Simple way to repeat 67 | * a string in java
68 | * People: Everybody who contributed but especially user102008

69 | */ 70 | public class DBTablePrinter { 71 | 72 | /** 73 | * Default maximum number of rows to query and print. 74 | */ 75 | private static final int DEFAULT_MAX_ROWS = 10; 76 | 77 | /** 78 | * Default maximum width for text columns 79 | * (like a VARCHAR) column. 80 | */ 81 | private static final int DEFAULT_MAX_TEXT_COL_WIDTH = 150; 82 | 83 | /** 84 | * Column type category for CHAR, VARCHAR 85 | * and similar text columns. 86 | */ 87 | public static final int CATEGORY_STRING = 1; 88 | 89 | /** 90 | * Column type category for TINYINT, SMALLINT, 91 | * INT and BIGINT columns. 92 | */ 93 | public static final int CATEGORY_INTEGER = 2; 94 | 95 | /** 96 | * Column type category for REAL, DOUBLE, 97 | * and DECIMAL columns. 98 | */ 99 | public static final int CATEGORY_DOUBLE = 3; 100 | 101 | /** 102 | * Column type category for date and time related columns like 103 | * DATE, TIME, TIMESTAMP etc. 104 | */ 105 | public static final int CATEGORY_DATETIME = 4; 106 | 107 | /** 108 | * Column type category for BOOLEAN columns. 109 | */ 110 | public static final int CATEGORY_BOOLEAN = 5; 111 | 112 | /** 113 | * Column type category for types for which the type name 114 | * will be printed instead of the content, like BLOB, 115 | * BINARY, ARRAY etc. 116 | */ 117 | public static final int CATEGORY_OTHER = 0; 118 | 119 | /** 120 | * Represents a database table column. 121 | */ 122 | private static class Column { 123 | 124 | /** 125 | * Column label. 126 | */ 127 | private String label; 128 | 129 | /** 130 | * Generic SQL type of the column as defined in 131 | * 133 | * java.sql.Types 134 | * . 135 | */ 136 | private int type; 137 | 138 | /** 139 | * Generic SQL type name of the column as defined in 140 | * 142 | * java.sql.Types 143 | * . 144 | */ 145 | private String typeName; 146 | 147 | /** 148 | * Width of the column that will be adjusted according to column label 149 | * and values to be printed. 150 | */ 151 | private int width = 0; 152 | 153 | /** 154 | * Column values from each row of a ResultSet. 155 | */ 156 | private List values = new ArrayList<>(); 157 | 158 | /** 159 | * Flag for text justification using String.format. 160 | * Empty string ("") to justify right, 161 | * dash (-) to justify left. 162 | * 163 | * @see #justifyLeft() 164 | */ 165 | private String justifyFlag = ""; 166 | 167 | /** 168 | * Column type category. The columns will be categorised according 169 | * to their column types and specific needs to print them correctly. 170 | */ 171 | private int typeCategory = 0; 172 | 173 | /** 174 | * Constructs a new Column with a column label, 175 | * generic SQL type and type name (as defined in 176 | * 178 | * java.sql.Types 179 | * ) 180 | * 181 | * @param label Column label or name 182 | * @param type Generic SQL type 183 | * @param typeName Generic SQL type name 184 | */ 185 | public Column(String label, int type, String typeName) { 186 | this.label = label; 187 | this.type = type; 188 | this.typeName = typeName; 189 | } 190 | 191 | /** 192 | * Returns the column label 193 | * 194 | * @return Column label 195 | */ 196 | public String getLabel() { 197 | return label; 198 | } 199 | 200 | /** 201 | * Returns the generic SQL type of the column 202 | * 203 | * @return Generic SQL type 204 | */ 205 | public int getType() { 206 | return type; 207 | } 208 | 209 | /** 210 | * Returns the generic SQL type name of the column 211 | * 212 | * @return Generic SQL type name 213 | */ 214 | public String getTypeName() { 215 | return typeName; 216 | } 217 | 218 | /** 219 | * Returns the width of the column 220 | * 221 | * @return Column width 222 | */ 223 | public int getWidth() { 224 | return width; 225 | } 226 | 227 | /** 228 | * Sets the width of the column to width 229 | * 230 | * @param width Width of the column 231 | */ 232 | public void setWidth(int width) { 233 | this.width = width; 234 | } 235 | 236 | /** 237 | * Adds a String representation (value) 238 | * of a value to this column object's {@link #values} list. 239 | * These values will come from each row of a 240 | * 242 | * ResultSet 243 | * of a database query. 244 | * 245 | * @param value The column value to add to {@link #values} 246 | */ 247 | public void addValue(String value) { 248 | values.add(value); 249 | } 250 | 251 | /** 252 | * Returns the column value at row index i. 253 | * Note that the index starts at 0 so that getValue(0) 254 | * will get the value for this column from the first row 255 | * of a 257 | * ResultSet. 258 | * 259 | * @param i The index of the column value to get 260 | * @return The String representation of the value 261 | */ 262 | public String getValue(int i) { 263 | return values.get(i); 264 | } 265 | 266 | /** 267 | * Returns the value of the {@link #justifyFlag}. The column 268 | * values will be printed using String.format and 269 | * this flag will be used to right or left justify the text. 270 | * 271 | * @return The {@link #justifyFlag} of this column 272 | * @see #justifyLeft() 273 | */ 274 | public String getJustifyFlag() { 275 | return justifyFlag; 276 | } 277 | 278 | /** 279 | * Sets {@link #justifyFlag} to "-" so that 280 | * the column value will be left justified when printed with 281 | * String.format. Typically numbers will be right 282 | * justified and text will be left justified. 283 | */ 284 | public void justifyLeft() { 285 | this.justifyFlag = "-"; 286 | } 287 | 288 | /** 289 | * Returns the generic SQL type category of the column 290 | * 291 | * @return The {@link #typeCategory} of the column 292 | */ 293 | public int getTypeCategory() { 294 | return typeCategory; 295 | } 296 | 297 | /** 298 | * Sets the {@link #typeCategory} of the column 299 | * 300 | * @param typeCategory The type category 301 | */ 302 | public void setTypeCategory(int typeCategory) { 303 | this.typeCategory = typeCategory; 304 | } 305 | } 306 | 307 | /** 308 | * Overloaded method that prints rows from table tableName 309 | * to standard out using the given database connection 310 | * conn. Total number of rows will be limited to 311 | * {@link #DEFAULT_MAX_ROWS} and 312 | * {@link #DEFAULT_MAX_TEXT_COL_WIDTH} will be used to limit 313 | * the width of text columns (like a VARCHAR column). 314 | * 315 | * @param conn Database connection object (java.sql.Connection) 316 | * @param tableName Name of the database table 317 | */ 318 | public static void printTable(Connection conn, String tableName) { 319 | printTable(conn, tableName, DEFAULT_MAX_ROWS, DEFAULT_MAX_TEXT_COL_WIDTH); 320 | } 321 | 322 | /** 323 | * Overloaded method that prints rows from table tableName 324 | * to standard out using the given database connection 325 | * conn. Total number of rows will be limited to 326 | * maxRows and 327 | * {@link #DEFAULT_MAX_TEXT_COL_WIDTH} will be used to limit 328 | * the width of text columns (like a VARCHAR column). 329 | * 330 | * @param conn Database connection object (java.sql.Connection) 331 | * @param tableName Name of the database table 332 | * @param maxRows Number of max. rows to query and print 333 | */ 334 | public static void printTable(Connection conn, String tableName, int maxRows) { 335 | printTable(conn, tableName, maxRows, DEFAULT_MAX_TEXT_COL_WIDTH); 336 | } 337 | 338 | /** 339 | * Overloaded method that prints rows from table tableName 340 | * to standard out using the given database connection 341 | * conn. Total number of rows will be limited to 342 | * maxRows and 343 | * maxStringColWidth will be used to limit 344 | * the width of text columns (like a VARCHAR column). 345 | * 346 | * @param conn Database connection object (java.sql.Connection) 347 | * @param tableName Name of the database table 348 | * @param maxRows Number of max. rows to query and print 349 | * @param maxStringColWidth Max. width of text columns 350 | */ 351 | public static void printTable(Connection conn, String tableName, int maxRows, int maxStringColWidth) { 352 | if (conn == null) { 353 | System.err.println("DBTablePrinter Error: No connection to database (Connection is null)!"); 354 | return; 355 | } 356 | if (tableName == null) { 357 | System.err.println("DBTablePrinter Error: No table name (tableName is null)!"); 358 | return; 359 | } 360 | if (tableName.length() == 0) { 361 | System.err.println("DBTablePrinter Error: Empty table name!"); 362 | return; 363 | } 364 | if (maxRows < 1) { 365 | System.err.println("DBTablePrinter Info: Invalid max. rows number. Using default!"); 366 | maxRows = DEFAULT_MAX_ROWS; 367 | } 368 | 369 | Statement stmt = null; 370 | ResultSet rs = null; 371 | try { 372 | if (conn.isClosed()) { 373 | System.err.println("DBTablePrinter Error: Connection is closed!"); 374 | return; 375 | } 376 | 377 | String sqlSelectAll = "SELECT * FROM " + tableName + " LIMIT " + maxRows; 378 | stmt = conn.createStatement(); 379 | rs = stmt.executeQuery(sqlSelectAll); 380 | 381 | printResultSet(rs, maxStringColWidth); 382 | 383 | } catch (SQLException e) { 384 | System.err.println("SQL exception in DBTablePrinter. Message:"); 385 | System.err.println(e.getMessage()); 386 | } finally { 387 | try { 388 | if (stmt != null) { 389 | stmt.close(); 390 | } 391 | if (rs != null) { 392 | rs.close(); 393 | } 394 | } catch (SQLException ignore) { 395 | // ignore 396 | } 397 | } 398 | } 399 | 400 | /** 401 | * Overloaded method to print rows of a 403 | * ResultSet to standard out using {@link #DEFAULT_MAX_TEXT_COL_WIDTH} 404 | * to limit the width of text columns. 405 | * 406 | * @param rs The ResultSet to print 407 | */ 408 | public static void printResultSet(ResultSet rs) { 409 | printResultSet(rs, DEFAULT_MAX_TEXT_COL_WIDTH); 410 | } 411 | 412 | /** 413 | * Overloaded method to print rows of a 415 | * ResultSet to standard out using maxStringColWidth 416 | * to limit the width of text columns. 417 | * 418 | * @param rs The ResultSet to print 419 | * @param maxStringColWidth Max. width of text columns 420 | */ 421 | public static void printResultSet(ResultSet rs, int maxStringColWidth) { 422 | try { 423 | if (rs == null) { 424 | System.err.println("DBTablePrinter Error: Result set is null!"); 425 | return; 426 | } 427 | if (rs.isClosed()) { 428 | System.err.println("DBTablePrinter Error: Result Set is closed!"); 429 | return; 430 | } 431 | if (maxStringColWidth < 1) { 432 | System.err.println("DBTablePrinter Info: Invalid max. varchar column width. Using default!"); 433 | maxStringColWidth = DEFAULT_MAX_TEXT_COL_WIDTH; 434 | } 435 | 436 | // Get the meta data object of this ResultSet. 437 | ResultSetMetaData rsmd; 438 | rsmd = rs.getMetaData(); 439 | 440 | // Total number of columns in this ResultSet 441 | int columnCount = rsmd.getColumnCount(); 442 | 443 | // List of Column objects to store each columns of the ResultSet 444 | // and the String representation of their values. 445 | List columns = new ArrayList<>(columnCount); 446 | 447 | // List of table names. Can be more than one if it is a joined 448 | // table query 449 | List tableNames = new ArrayList<>(columnCount); 450 | 451 | // Get the columns and their meta data. 452 | // NOTE: columnIndex for rsmd.getXXX methods STARTS AT 1 NOT 0 453 | for (int i = 1; i <= columnCount; i++) { 454 | Column c = new Column(rsmd.getColumnLabel(i), 455 | rsmd.getColumnType(i), rsmd.getColumnTypeName(i)); 456 | c.setWidth(c.getLabel().length()); 457 | c.setTypeCategory(whichCategory(c.getType())); 458 | columns.add(c); 459 | 460 | if (!tableNames.contains(rsmd.getTableName(i))) { 461 | tableNames.add(rsmd.getTableName(i)); 462 | } 463 | } 464 | 465 | // Go through each row, get values of each column and adjust 466 | // column widths. 467 | int rowCount = 0; 468 | while (rs.next()) { 469 | 470 | System.out.println("row: " + rowCount); 471 | 472 | // NOTE: columnIndex for rs.getXXX methods STARTS AT 1 NOT 0 473 | for (int i = 0; i < columnCount; i++) { 474 | Column c = columns.get(i); 475 | String value; 476 | int category = c.getTypeCategory(); 477 | 478 | if (category == CATEGORY_OTHER) { 479 | 480 | // Use generic SQL type name instead of the actual value 481 | // for column types BLOB, BINARY etc. 482 | value = "(" + c.getTypeName() + ")"; 483 | 484 | } else { 485 | value = rs.getString(i + 1) == null ? "NULL" : rs.getString(i + 1); 486 | } 487 | switch (category) { 488 | case CATEGORY_DOUBLE: 489 | 490 | // For real numbers, format the string value to have 3 digits 491 | // after the point. THIS IS TOTALLY ARBITRARY and can be 492 | // improved to be CONFIGURABLE. 493 | if (!value.equals("NULL")) { 494 | Double dValue = rs.getDouble(i + 1); 495 | value = String.format("%.3f", dValue); 496 | } 497 | break; 498 | 499 | case CATEGORY_STRING: 500 | 501 | // Left justify the text columns 502 | c.justifyLeft(); 503 | 504 | // and apply the width limit 505 | if (value.length() > maxStringColWidth) { 506 | value = value.substring(0, maxStringColWidth - 3) + "..."; 507 | } 508 | break; 509 | } 510 | 511 | // Adjust the column width 512 | c.setWidth(value.length() > c.getWidth() ? value.length() : c.getWidth()); 513 | c.addValue(value); 514 | } // END of for loop columnCount 515 | rowCount++; 516 | 517 | } // END of while (rs.next) 518 | 519 | /* 520 | At this point we have gone through meta data, get the 521 | columns and created all Column objects, iterated over the 522 | ResultSet rows, populated the column values and adjusted 523 | the column widths. 524 | 525 | We cannot start printing just yet because we have to prepare 526 | a row separator String. 527 | */ 528 | 529 | // For the fun of it, I will use StringBuilder 530 | StringBuilder strToPrint = new StringBuilder(); 531 | StringBuilder rowSeparator = new StringBuilder(); 532 | 533 | /* 534 | Prepare column labels to print as well as the row separator. 535 | It should look something like this: 536 | +--------+------------+------------+-----------+ (row separator) 537 | | EMP_NO | BIRTH_DATE | FIRST_NAME | LAST_NAME | (labels row) 538 | +--------+------------+------------+-----------+ (row separator) 539 | */ 540 | 541 | // Iterate over columns 542 | for (Column c : columns) { 543 | int width = c.getWidth(); 544 | 545 | // Center the column label 546 | String toPrint; 547 | String name = c.getLabel(); 548 | int diff = width - name.length(); 549 | 550 | if ((diff % 2) == 1) { 551 | // diff is not divisible by 2, add 1 to width (and diff) 552 | // so that we can have equal padding to the left and right 553 | // of the column label. 554 | width++; 555 | diff++; 556 | c.setWidth(width); 557 | } 558 | 559 | int paddingSize = diff / 2; // InteliJ says casting to int is redundant. 560 | 561 | // Cool String repeater code thanks to user102008 at stackoverflow.com 562 | // (http://tinyurl.com/7x9qtyg) "Simple way to repeat a string in java" 563 | String padding = new String(new char[paddingSize]).replace("\0", " "); 564 | 565 | toPrint = "| " + padding + name + padding + " "; 566 | // END centering the column label 567 | 568 | strToPrint.append(toPrint); 569 | 570 | rowSeparator.append("+"); 571 | rowSeparator.append(new String(new char[width + 2]).replace("\0", "-")); 572 | } 573 | 574 | String lineSeparator = System.getProperty("line.separator"); 575 | 576 | // Is this really necessary ?? 577 | lineSeparator = lineSeparator == null ? "\n" : lineSeparator; 578 | 579 | rowSeparator.append("+").append(lineSeparator); 580 | 581 | strToPrint.append("|").append(lineSeparator); 582 | strToPrint.insert(0, rowSeparator); 583 | strToPrint.append(rowSeparator); 584 | 585 | StringJoiner sj = new StringJoiner(", "); 586 | for (String name : tableNames) { 587 | sj.add(name); 588 | } 589 | 590 | String info = "Printing " + rowCount; 591 | info += rowCount > 1 ? " rows from " : " row from "; 592 | info += tableNames.size() > 1 ? "tables " : "table "; 593 | info += sj.toString(); 594 | 595 | System.out.println(info); 596 | 597 | // Print out the formatted column labels 598 | System.out.print(strToPrint.toString()); 599 | 600 | String format; 601 | 602 | // Print out the rows 603 | for (int i = 0; i < rowCount; i++) { 604 | for (Column c : columns) { 605 | 606 | // This should form a format string like: "%-60s" 607 | format = String.format("| %%%s%ds ", c.getJustifyFlag(), c.getWidth()); 608 | System.out.print( 609 | String.format(format, c.getValue(i)) 610 | ); 611 | } 612 | 613 | System.out.println("|"); 614 | System.out.print(rowSeparator); 615 | } 616 | 617 | System.out.println(); 618 | 619 | /* 620 | Hopefully this should have printed something like this: 621 | +--------+------------+------------+-----------+--------+-------------+ 622 | | EMP_NO | BIRTH_DATE | FIRST_NAME | LAST_NAME | GENDER | HIRE_DATE | 623 | +--------+------------+------------+-----------+--------+-------------+ 624 | | 10001 | 1953-09-02 | Georgi | Facello | M | 1986-06-26 | 625 | +--------+------------+------------+-----------+--------+-------------+ 626 | | 10002 | 1964-06-02 | Bezalel | Simmel | F | 1985-11-21 | 627 | +--------+------------+------------+-----------+--------+-------------+ 628 | */ 629 | 630 | } catch (SQLException e) { 631 | System.err.println("SQL exception in DBTablePrinter. Message:"); 632 | System.err.println(e.getMessage()); 633 | } 634 | } 635 | 636 | /** 637 | * Takes a generic SQL type and returns the category this type 638 | * belongs to. Types are categorized according to print formatting 639 | * needs: 640 | *

641 | * Integers should not be truncated so column widths should 642 | * be adjusted without a column width limit. Text columns should be 643 | * left justified and can be truncated to a max. column width etc...

644 | *

645 | * See also: 647 | * java.sql.Types 648 | * 649 | * @param type Generic SQL type 650 | * @return The category this type belongs to 651 | */ 652 | private static int whichCategory(int type) { 653 | switch (type) { 654 | case Types.BIGINT: 655 | case Types.TINYINT: 656 | case Types.SMALLINT: 657 | case Types.INTEGER: 658 | return CATEGORY_INTEGER; 659 | 660 | case Types.REAL: 661 | case Types.DOUBLE: 662 | case Types.DECIMAL: 663 | return CATEGORY_DOUBLE; 664 | 665 | case Types.DATE: 666 | case Types.TIME: 667 | case Types.TIME_WITH_TIMEZONE: 668 | case Types.TIMESTAMP: 669 | case Types.TIMESTAMP_WITH_TIMEZONE: 670 | return CATEGORY_DATETIME; 671 | 672 | case Types.BOOLEAN: 673 | return CATEGORY_BOOLEAN; 674 | 675 | case Types.VARCHAR: 676 | case Types.NVARCHAR: 677 | case Types.LONGVARCHAR: 678 | case Types.LONGNVARCHAR: 679 | case Types.CHAR: 680 | case Types.NCHAR: 681 | return CATEGORY_STRING; 682 | 683 | default: 684 | return CATEGORY_OTHER; 685 | } 686 | } 687 | } 688 | --------------------------------------------------------------------------------