Skip to content

Commit

Permalink
implement ConnectionProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanQingyangXu authored and nathan.xu committed Oct 27, 2024
1 parent dcb1833 commit 86d2836
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 5 deletions.
8 changes: 3 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,16 @@
import net.ltgt.gradle.errorprone.errorprone

plugins {
// Apply the java-library plugin for API and implementation separation.
`java-library`
alias(libs.plugins.spotless)
alias(libs.plugins.errorprone)
}

repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}

dependencies {
// Use JUnit Jupiter for testing.
testImplementation(libs.junit.jupiter)

testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand All @@ -38,17 +35,18 @@ dependencies {
api(libs.jspecify)

errorprone(libs.google.errorprone.core)

implementation(libs.hibernate.core)
implementation(libs.mongo.java.driver.sync)
}

// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ errorprone = "4.0.1"
google-errorprone-core = "2.9.0"
nullaway = "0.10.26"
jspecify = "0.3.0"
hibernate-core = "6.6.1.Final"
mongo-java-driver-sync = "5.2.0"

[libraries]
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" }
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
google-errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "google-errorprone-core" }
hibernate-core = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate-core" }
mongo-java-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongo-java-driver-sync" }

[plugins]
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/com/mongodb/hibernate/cfg/ConfigurationHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2024-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.hibernate.cfg;

import static org.hibernate.internal.util.NullnessUtil.castNonNull;

import com.mongodb.hibernate.exception.ConfigurationException;
import java.util.Map;
import org.jspecify.annotations.Nullable;

/** Collection of helper methods for dealing with configuration settings. */
public final class ConfigurationHelper {

private ConfigurationHelper() {}

public static String getRequiredConfiguration(Map<String, Object> configurationValues, String property) {
return castNonNull(doGetConfiguration(configurationValues, property, true));
}

public static @Nullable String getOptionalConfiguration(Map<String, Object> configurationValues, String property) {
return doGetConfiguration(configurationValues, property, false);
}

private static @Nullable String doGetConfiguration(
Map<String, Object> configurationValues, String property, boolean required) {
var configuration = configurationValues.get(property);
if (configuration == null && required) {
throw new ConfigurationException(property, "value required");
}
if (!(configuration instanceof String)) {
throw new ConfigurationException(property, "value is not of string type");
}
return (String) configuration;
}
}
40 changes: 40 additions & 0 deletions src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024-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.hibernate.dialect;

import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;

public class MongoDialect extends Dialect {
public static final int MINIMUM_MONGODB_MAJOR_VERSION_SUPPORTED = 5;

private static final DatabaseVersion MINIMUM_VERSION =
DatabaseVersion.make(MINIMUM_MONGODB_MAJOR_VERSION_SUPPORTED);

public MongoDialect() {
this(MINIMUM_VERSION);
}

public MongoDialect(final DatabaseVersion version) {
super(version);
}

public MongoDialect(final DialectResolutionInfo dialectResolutionInfo) {
super(dialectResolutionInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024-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.hibernate.exception;

public class ConfigurationException extends RuntimeException {

private final String property;

public ConfigurationException(String property, String message) {
super(message);
this.property = property;
}

public ConfigurationException(String property, String message, Throwable cause) {
super(message, cause);
this.property = property;
}

@Override
public String getMessage() {
return String.format("Invalid '%s' configuration: %s", property, super.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2024-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.hibernate.exception;

public class NotYetImplementedException extends RuntimeException {

public NotYetImplementedException() {}

public NotYetImplementedException(String message) {
super(message);
}
}
149 changes: 149 additions & 0 deletions src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright 2024-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.hibernate.jdbc;

import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
import static org.hibernate.cfg.JdbcSettings.*;
import static org.hibernate.internal.util.NullnessUtil.castNonNull;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.hibernate.cfg.ConfigurationHelper;
import com.mongodb.hibernate.exception.ConfigurationException;
import com.mongodb.hibernate.exception.NotYetImplementedException;
import java.sql.Connection;
import java.util.Map;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.service.UnknownUnwrapTypeException;
import org.hibernate.service.spi.Configurable;
import org.hibernate.service.spi.Startable;
import org.hibernate.service.spi.Stoppable;
import org.jspecify.annotations.Nullable;

/**
* MongoDB dialect's customized JDBC {@link ConnectionProvider} spi implementation, whose class name is supposed to be
* provided as the following Hibernate property to kick off MongoDB dialect's JDBC flow:
*
* <ul>
* <li>hibernate.connection.provider_class
* </ul>
*
* <p>The following Hibernate JDBC properties will be relied upon by Hibernate's {@link Configurable} spi mechanism:
*
* <ul>
* <li>jakarta.persistence.jdbc.url
* <li>jakarta.persistence.jdbc.user
* <li>jakarta.persistence.jdbc.password
* </ul>
*
* <p><code>jakarta.persistence.jdbc.url</code> property is mandatory and it maps to MongoDB's {@link ConnectionString},
* in which database name must be provided to align with JDBC URL's convention. The other two JDBC properties are
* optional.
*
* @see ConnectionProvider
* @see Configurable
*/
public class MongoConnectionProvider implements ConnectionProvider, Configurable, Startable, Stoppable {

// non-null after configure(Map<String, Object>) method is invoked successfully
private @Nullable ConnectionString connectionString;
private @Nullable String database;

// non-null after start() method is invoked successfully
private @Nullable MongoClient mongoClient;

private @Nullable String user;
private @Nullable String password;

@Override
public Connection getConnection() {
throw new NotYetImplementedException();
}

@Override
public void closeConnection(Connection connection) {
throw new NotYetImplementedException();
}

@Override
public boolean supportsAggressiveRelease() {
return false; // won't be used in container
}

@Override
public boolean isUnwrappableAs(Class<?> unwrapType) {
return ConnectionProvider.class.equals(unwrapType)
|| MongoConnectionProvider.class.isAssignableFrom(unwrapType);
}

@Override
public <T> T unwrap(Class<T> unwrapType) {
if (isUnwrappableAs(unwrapType)) {
return unwrapType.cast(this);
} else {
throw new UnknownUnwrapTypeException(unwrapType);
}
}

@Override
public void configure(Map<String, Object> configurationValues) {
var jdbcUrl = ConfigurationHelper.getRequiredConfiguration(configurationValues, JAKARTA_JDBC_URL);
try {
this.connectionString = new ConnectionString(jdbcUrl);
} catch (IllegalArgumentException iae) {
throw new ConfigurationException(JAKARTA_JDBC_URL, "invalid MongoDB connection string", iae);
}
var database = this.connectionString.getDatabase();
if (database == null) {
throw new ConfigurationException(JAKARTA_JDBC_URL, "database must be provided");
}
this.database = database;
this.user = ConfigurationHelper.getOptionalConfiguration(configurationValues, JAKARTA_JDBC_USER);
this.password = ConfigurationHelper.getOptionalConfiguration(configurationValues, JAKARTA_JDBC_PASSWORD);
}

@Override
public void start() {
// connectionString and database are set as mandatory values in the above configure method
// if either is unset, exception would have been thrown and this method invocation would have be skipped
castNonNull(this.connectionString);
castNonNull(this.database);

var clientSettingsBuilder = MongoClientSettings.builder().applyConnectionString(connectionString);
if (this.user != null) {
var password = this.password == null ? null : this.password.toCharArray();
var credential = MongoCredential.createCredential(this.user, this.database, password);
clientSettingsBuilder.credential(credential);
}

var codecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry());
clientSettingsBuilder.codecRegistry(codecRegistry);

var clientSettings = clientSettingsBuilder.build();
this.mongoClient = MongoClients.create(clientSettings);
}

@Override
public void stop() {
if (this.mongoClient != null) {
this.mongoClient.close();
}
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/mongodb/hibernate/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package com.mongodb.hibernate;

import org.jspecify.annotations.NullMarked;
Loading

0 comments on commit 86d2836

Please sign in to comment.