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}