All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mongodb.jdbc.MongoDriver Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
/*
 * Copyright 2020-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.jdbc;

import static org.bson.codecs.configuration.CodecRegistries.fromProviders;

import com.mongodb.ConnectionString;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Arrays;
import java.util.Properties;
import org.bson.codecs.BsonValueCodecProvider;
import org.bson.codecs.ValueCodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;

/**
 * The MongoDriver implements the java.sql.Driver interface, which allows for opening Connections to
 * MonogDB databases that have SQL support.
 *
 * @see DriverManager
 * @see Connection
 * @since 1.0.0
 */
public class MongoDriver implements Driver {
    /** All MongoDB SQL URLs must begin with jdbc:mongodb: */
    static final String JDBC = "jdbc:";

    static final String MONGODB_URL_PREFIX = JDBC + "mongodb:";
    static final String MONGODB_SRV_URL_PREFIX = JDBC + "mongodb+srv:";
    static final String USER = "user";
    static final String PASSWORD = "password";
    static final String CONVERSION_MODE = "conversionMode";
    // database is the database to switch to.
    static final String DATABASE = "database";

    static CodecRegistry registry =
            fromProviders(
                    new BsonValueCodecProvider(),
                    new ValueCodecProvider(),
                    PojoCodecProvider.builder().automatic(true).build());

    static {
        try {
            DriverManager.registerDriver(new MongoDriver());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        if (!acceptsURL(url)) {
            return null;
        }
        if (info == null) {
            info = new Properties();
        }
        // reuse the code getPropertyInfo to make sure the URI is properly set wrt the passed
        // Properties info value.
        Pair p = getConnectionString(url, info);
        // since the user is calling connect, we should throw an SQLException if we get
        // a prompt back. Inspect the return value to format the SQLException.
        DriverPropertyInfo[] driverPropertyInfo = p.right();
        if (driverPropertyInfo.length != 0) {
            if (driverPropertyInfo[0].name.equals(USER)) {
                throw new SQLException("password specified without user");
            }
            if (driverPropertyInfo[0].name.equals(PASSWORD)) {
                throw new SQLException("user specified without password");
            }
            String[] propertyNames = new String[driverPropertyInfo.length];
            for (int i = 0; i < propertyNames.length; ++i) {
                propertyNames[i] = driverPropertyInfo[i].name;
            }
            throw new SQLException(
                    "unexpected driver property info prompt returned: "
                            + String.join(", ", propertyNames));
        }
        return new MongoConnection(
                p.left(), info.getProperty(DATABASE), info.getProperty(CONVERSION_MODE));
    }

    @Override
    public boolean acceptsURL(String url) throws SQLException {
        return url.startsWith(MONGODB_URL_PREFIX) || url.startsWith(MONGODB_SRV_URL_PREFIX);
    }

    @Override
    public DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
            throws SQLException {
        Pair p = getConnectionString(url, info);
        return p.right();
    }

    @Override
    public int getMajorVersion() {
        return 1;
    }

    @Override
    public int getMinorVersion() {
        return 0;
    }

    @Override
    public boolean jdbcCompliant() {
        return false;
    }

    // ------------------------- JDBC 4.1 -----------------------------------

    @Override
    public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new SQLFeatureNotSupportedException();
    }

    // removePrefix removes a prefix from a String.
    private static String removePrefix(String prefix, String s) {
        if (s != null && prefix != null && s.startsWith(prefix)) {
            return s.substring(prefix.length());
        }
        return s;
    }

    private Pair getConnectionString(
            String url, java.util.Properties info) throws SQLException {
        if (info == null) {
            info = new Properties();
        }
        String actualURL = removePrefix(JDBC, url);
        ConnectionString originalConnectionString;
        try {
            originalConnectionString = new ConnectionString(actualURL);
        } catch (Exception e) {
            throw new SQLException(e);
        }
        String authDatabase = originalConnectionString.getDatabase();
        Pair clientProperties = extractProperties(originalConnectionString, info);
        String user = clientProperties.left();
        char[] password = clientProperties.right();
        // Attempt to get an options string from the url string, itself, so that we do
        // not need to format the options returned by the ConnectionString.
        String optionString = null;
        String[] optionSplit =
                actualURL.split("[?]"); // split takes a regexp and '?' is a metachar.
        if (optionSplit.length > 1) {
            optionString = optionSplit[1];
        }

        if (user == null && password == null) {
            ConnectionString c =
                    new ConnectionString(
                            buildNewURI(
                                    originalConnectionString,
                                    user,
                                    password,
                                    authDatabase,
                                    optionString));
            return new Pair<>(c, new DriverPropertyInfo[] {});
        }
        if (user == null) {
            // user is null, but password is not, we must prompt for the user.
            // Note: The convention is actually to return DriverPropertyInfo objects
            // with null values, this is not a bug.
            return new Pair<>(null, new DriverPropertyInfo[] {new DriverPropertyInfo(USER, null)});
        }
        if (password == null) {
            // If password is null here, then user name must be non-null,because
            // the both null case is handled above. Prompt for the password.
            return new Pair<>(
                    null, new DriverPropertyInfo[] {new DriverPropertyInfo(PASSWORD, null)});
        }
        // If we are here, we must have both a user and password. So we have a valid URI state,
        // go ahead and construct it and prompt for nothing.
        ConnectionString c =
                new ConnectionString(
                        buildNewURI(
                                originalConnectionString,
                                user,
                                password,
                                authDatabase,
                                optionString));
        return new Pair<>(c, new DriverPropertyInfo[] {});
    }

    private static interface NullCoalesce {
        T coalesce(T left, T right);
    }
    // private helper function to abstract checking consistency between properties and the URI, and
    // grabbing the relevant data.
    //
    // throws SQLException if url and properties disagree on user or password.
    private static Pair extractProperties(
            ConnectionString clientURI, Properties info) throws SQLException {

        // The coalesce function takse the first non-null argument, returning null only
        // if both arguments are null. The java type system requires us to write this twice,
        // once for each type we care about, unless we prefer to use Objects and cast, but I avoid
        // that.
        NullCoalesce s =
                (left, right) -> {
                    if (left == null) {
                        return right;
                    }
                    return left;
                };

        NullCoalesce c =
                (left, right) -> {
                    if (left == null) {
                        return right;
                    }
                    return left;
                };

        // grab the user and password from the URI.
        String uriUser = clientURI.getUsername();
        char[] uriPWD = clientURI.getPassword();
        String uriAuthDatabase = clientURI.getDatabase();
        String propertyUser = info.getProperty(USER);
        String propertyPWDStr = info.getProperty(PASSWORD);
        char[] propertyPWD = propertyPWDStr != null ? propertyPWDStr.toCharArray() : null;
        // handle disagreements on user.
        if (uriUser != null && propertyUser != null && !uriUser.equals(propertyUser)) {
            throw new SQLException(
                    "uri and properties disagree on user: '"
                            + uriUser
                            + ", and "
                            + propertyUser
                            + " respectively");
        }
        // set the user
        String user = s.coalesce(uriUser, propertyUser);
        // handle disagreements on password.
        if (uriPWD != null && propertyPWD != null && !Arrays.equals(uriPWD, propertyPWD)) {
            throw new SQLException("uri and properties disagree on password");
        }
        // set the password
        char[] password = c.coalesce(uriPWD, propertyPWD);
        return new Pair<>(user, password);
    }

    // This is just a clean abstraction around URLEncode.
    private static String sqlURLEncode(String item) throws SQLException {
        try {
            return URLEncoder.encode(item, "utf-8");
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }

    // This function builds a new uri from the original clientURI, adding user, password, options, and
    // database, if necessary.
    private static String buildNewURI(
            ConnectionString originalConnectionString,
            String user,
            char[] password,
            String authDatabase,
            String optionsString)
            throws SQLException {
        // The returned URI should be of the following format:
        //"mongodb://[user:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[authDatabase][?options]]")
        String ret = "mongodb://";
        if (user != null) {
            // Note: if user is not null, we already know that password must also be not null.
            ret += sqlURLEncode(user) + ":" + sqlURLEncode(String.valueOf(password)) + "@";
        }
        // Now add hosts.
        ret += String.join(",", originalConnectionString.getHosts());
        // Now add authDatabase, if necessary.
        if (authDatabase != null) {
            ret += "/" + sqlURLEncode(authDatabase);
        }
        // OptionsString should already be properly encoded, since we got it straight from the url
        // string.
        if (optionsString != null) {
            ret += "?" + optionsString;
        }
        return ret;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy