001/* 002 * Copyright (c) 2026, Oracle and/or its affiliates. 003 * 004 * Licensed under the Universal Permissive License Version 1.0 as shown at 005 * https://oss.oracle.com/licenses/upl/ 006 */ 007package com.oracle.database.selectai.model; 008 009import java.util.Locale; 010import java.util.Properties; 011 012/** 013 * Immutable configuration for creating an SDK-owned Oracle JDBC connection. 014 * <p> 015 * Use this configuration with {@link com.oracle.database.selectai.SelectAI#create(DbConnectionConfig)} 016 * or {@link com.oracle.database.selectai.DatabaseAdmin#create(DbConnectionConfig)} when the 017 * SDK should open and own one JDBC connection. For pooled or multi-threaded 018 * applications, prefer the {@link javax.sql.DataSource}-based factory methods. 019 * <p> 020 * Password fields are retained in memory as {@link String} values so they can 021 * be passed to Oracle JDBC. Do not log, persist, or expose this configuration 022 * in diagnostics. 023 */ 024public final class DbConnectionConfig { 025 026 /** Database username used to connect to Autonomous Database. */ 027 private final String dbUser; 028 /** Database password used to connect to Autonomous Database. */ 029 private final String dbPassword; 030 /** Optional password for password-protected wallets. */ 031 private final String walletPassword; 032 /** Full JDBC URL used when opening the SDK-owned JDBC connection. */ 033 private final String jdbcUrl; 034 /** Additional JDBC connection properties passed to DriverManager. */ 035 private final Properties jdbcProperties; 036 037 private DbConnectionConfig(Builder builder) { 038 this.dbUser = builder.dbUser; 039 this.dbPassword = builder.dbPassword; 040 this.walletPassword = builder.walletPassword; 041 this.jdbcUrl = builder.jdbcUrl; 042 this.jdbcProperties = copyProperties(builder.jdbcProperties); 043 } 044 045 /** 046 * Returns the database username. 047 * 048 * @return configured database username 049 */ 050 public String getDbUser() { 051 return dbUser; 052 } 053 054 /** 055 * Returns the database password. 056 * <p> 057 * This value is sensitive. Do not log, persist, or expose it in diagnostics. 058 * 059 * @return configured database password 060 */ 061 public String getDbPassword() { 062 return dbPassword; 063 } 064 065 /** 066 * Returns the password for a password-protected wallet. 067 * <p> 068 * This value is sensitive. Do not log, persist, or expose it in diagnostics. 069 * 070 * @return configured wallet password, or {@code null} when the wallet is not password-protected 071 */ 072 public String getWalletPassword() { 073 return walletPassword; 074 } 075 076 /** 077 * Returns the JDBC URL. 078 * 079 * @return configured JDBC URL 080 */ 081 public String getJdbcUrl() { 082 return jdbcUrl; 083 } 084 085 086 /** 087 * Returns additional JDBC connection properties. 088 * <p> 089 * The returned {@link Properties} object is a defensive copy. Changes to it 090 * do not affect this connection configuration. Property values may contain 091 * sensitive data, so callers should not log or expose the returned object. 092 * 093 * @return configured JDBC properties; never {@code null} 094 */ 095 public Properties getJdbcProperties() { 096 return copyProperties(jdbcProperties); 097 } 098 099 /** 100 * Creates a builder for database connection configuration. 101 * <p> 102 * The builder does not provide default credentials, wallet locations, or 103 * database names. Callers must supply connection values explicitly. 104 * 105 * @return new builder with no connection values populated 106 */ 107 public static Builder builder() { 108 return new Builder(); 109 } 110 111 /** 112 * Builder for {@link DbConnectionConfig}. 113 */ 114 public static final class Builder { 115 116 /** Database username to place in the final connection configuration. */ 117 private String dbUser; 118 /** Database password to place in the final connection configuration. */ 119 private String dbPassword; 120 /** Optional password for password-protected wallets. */ 121 private String walletPassword; 122 /** JDBC URL used by the final connection configuration. */ 123 private String jdbcUrl; 124 /** Additional JDBC connection properties passed to DriverManager. */ 125 private Properties jdbcProperties = new Properties(); 126 127 128 private Builder() { 129 } 130 131 /** 132 * Sets the database username. 133 * 134 * @param dbUser database username 135 * @return this builder instance 136 */ 137 public Builder dbUser(String dbUser) { 138 this.dbUser = dbUser; 139 return this; 140 } 141 142 /** 143 * Sets the database password. 144 * 145 * @param dbPassword database password 146 * @return this builder instance 147 */ 148 public Builder dbPassword(String dbPassword) { 149 this.dbPassword = dbPassword; 150 return this; 151 } 152 153 /** 154 * Sets the password for a password-protected wallet. 155 * <p> 156 * The SDK passes this value to Oracle JDBC as 157 * {@code oracle.net.wallet_password}; it is not appended to the JDBC URL. 158 * 159 * @param walletPassword wallet password, required only for password-protected wallets 160 * @return this builder instance 161 */ 162 public Builder walletPassword(String walletPassword) { 163 this.walletPassword = walletPassword; 164 return this; 165 } 166 167 /** 168 * Sets an explicit JDBC URL. 169 * <p> 170 * Supported URL forms include Oracle JDBC Thin Easy Connect/Easy 171 * Connect Plus, TNS aliases, full connection descriptors, TCPS 172 * configurations, and wallet/TNS_ADMIN based Autonomous Database URLs. 173 * This builder validates only the SDK-owned minimum contract: the URL 174 * must be an Oracle Thin JDBC URL with a non-blank connect target after 175 * {@code @}. Driver-level network, wallet, TLS, Kerberos, RADIUS, and 176 * token-authentication requirements are validated by Oracle JDBC and 177 * the database environment. 178 * 179 * @param jdbcUrl full JDBC URL 180 * @return this builder instance 181 */ 182 public Builder jdbcUrl(String jdbcUrl) { 183 this.jdbcUrl = normalize(jdbcUrl); 184 return this; 185 } 186 187 /** 188 * Sets additional JDBC properties to use when creating the database 189 * connection through {@code DriverManager}. 190 * 191 * <p>The supplied properties are validated and defensively copied so that 192 * subsequent modifications to the caller's {@link Properties} instance do 193 * not affect this configuration. If {@code jdbcProperties} is {@code null}, 194 * an empty set of JDBC properties is used.</p> 195 * 196 * <p>The SDK owns the {@code user} and {@code password} properties from 197 * {@link #dbUser(String)} and {@link #dbPassword(String)}. Supplying those 198 * keys through this method is rejected. Other driver-specific property 199 * names and values are passed through to Oracle JDBC for validation. Do not 200 * log properties that contain sensitive values.</p> 201 * 202 * @param jdbcProperties additional JDBC connection properties, or 203 * {@code null} to use no additional properties 204 * @return this builder instance 205 * @throws IllegalArgumentException if the supplied properties contain 206 * blank property names or SDK-owned 207 * {@code user} or {@code password} keys 208 */ 209 public Builder jdbcProperties(Properties jdbcProperties) { 210 if (jdbcProperties == null) { 211 this.jdbcProperties = new Properties(); 212 return this; 213 } 214 validateJdbcProperties(jdbcProperties); 215 this.jdbcProperties = copyProperties(jdbcProperties); 216 return this; 217 } 218 219 /** 220 * Validates required connection fields and builds the immutable config. 221 * 222 * @return immutable DbConnectionConfig built from validated builder state 223 */ 224 public DbConnectionConfig build() { 225 if (dbUser == null || dbUser.isBlank()) { 226 throw new IllegalArgumentException("dbUser must not be null or blank"); 227 } 228 if (dbPassword == null || dbPassword.isBlank()) { 229 throw new IllegalArgumentException("dbPassword must not be null or blank"); 230 } 231 if (walletPassword != null && walletPassword.isBlank()) { 232 throw new IllegalArgumentException("walletPassword must not be blank"); 233 } 234 if (jdbcUrl == null || jdbcUrl.isBlank()) { 235 throw new IllegalArgumentException("jdbcUrl must not be null or blank"); 236 } 237 validateJdbcUrl(jdbcUrl); 238 return new DbConnectionConfig(this); 239 } 240 241 private static String normalize(String value) { 242 if (value == null) { 243 return null; 244 } 245 String trimmed = value.trim(); 246 return trimmed.isEmpty() ? null : trimmed; 247 } 248 249 private static void validateJdbcUrl(String jdbcUrl) { 250 String normalized = normalize(jdbcUrl); 251 if (normalized == null) { 252 throw new IllegalArgumentException("jdbcUrl must not be null or blank"); 253 } 254 String lowerCaseUrl = normalized.toLowerCase(Locale.ROOT); 255 String prefix = "jdbc:oracle:thin:@"; 256 if (!lowerCaseUrl.startsWith(prefix)) { 257 throw new IllegalArgumentException("jdbcUrl must start with jdbc:oracle:thin:@"); 258 } 259 String connectTarget = normalized.substring(prefix.length()).trim(); 260 if (connectTarget.isEmpty()) { 261 throw new IllegalArgumentException("jdbcUrl must include a connect target after jdbc:oracle:thin:@"); 262 } 263 } 264 265 private static void validateJdbcProperties(Properties properties) { 266 if (properties == null) { 267 return; 268 } 269 for (Object key : properties.keySet()) { 270 String normalizedKey = String.valueOf(key).trim().toLowerCase(Locale.ROOT); 271 if (normalizedKey.isBlank()) { 272 throw new IllegalArgumentException("JDBC property names must not be blank"); 273 } 274 if ("user".equals(normalizedKey) || "password".equals(normalizedKey)) { 275 throw new IllegalArgumentException( 276 "JDBC properties must not contain user or password"); 277 } 278 } 279 } 280 } 281 private static Properties copyProperties(Properties source) { 282 Properties copy = new Properties(); 283 if (source != null) { 284 copy.putAll(source); 285 } 286 return copy; 287 } 288}