From caf40017b283b003e42ea72ab20ac8abbcb5b38d Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 14 Nov 2024 15:16:03 -0500 Subject: [PATCH] implement ConnectionProvider (#8) * implement ConnectionProvider * change minimal MongoDB version to 6 * Update src/test/resources/hibernate.properties Co-authored-by: Jeff Yemin * remove package-info.java in test folder; add copyright to package-info.java in main folder * improve Javadoc for MongoConnectionProvider per review comments * remove unnecessary codec setting stuff * simplify the dummy implementation of the three methods irrelevant to our usage * simplify MongoConnectionProvider per code review comments * remove exception package and move ConfigurationException to cfg package * add missing Javadocs for MongoDialect * add MQL acronym expansion in Javadoc * round #2 code review comments * change mininum mongodb version to v6.3 * use HibernateException instead of ConfigurationException.java * switch MongoDialect minimum db version to 6.0 * Update src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java Co-authored-by: Jeff Yemin * deleted unused constructors in MongoDialect * add Javadoc for the root package-info.java * make more exception messages begin with uppercase * update various gradle plugin to latest versions * add sl4j logging dependencies and logback-test.xml config * fix a static check issue * change default constructor of MongoDialect to end up with null value; override getMinimumSupportedVersion() * some minor improvements * change as per Valentin's excellent code review comments * update SessionFactoryTests as per code review comments * update MongoDialect as per code review comments * update MongoConnectionProvider as per code review comments * delete .editorconfig file and move it to another separate PR * fix a static check style issue * resolve some code review comments * fix style checking issue * minor changes to improve Dialect min version usage * improve mentioning of MongoDB Java Driver in MongoDialect's Javadoc * add transient keyword to MongoClient in MongoConnectionProvider * Update src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java Co-authored-by: Valentin Kovalenko * change as per code review comments * remove database checking logic * fix import style issue * change per some code review comments * add missing 'compileJava' gradle task in evergreen's static-check.sh --------- Co-authored-by: Jeff Yemin Co-authored-by: Valentin Kovalenko --- .evergreen/static-check.sh | 2 +- build.gradle.kts | 15 +- gradle/libs.versions.toml | 19 ++- .../hibernate/dialect/MongoDialect.java | 52 ++++++ .../hibernate/dialect/package-info.java | 20 +++ .../internal/NotYetImplementedException.java | 53 ++++++ .../hibernate/internal/package-info.java | 20 +++ .../jdbc/MongoConnectionProvider.java | 153 ++++++++++++++++++ .../mongodb/hibernate/jdbc/package-info.java | 20 +++ .../hibernate/SessionFactoryTests.java | 64 ++++++++ src/test/resources/logback-test.xml | 15 ++ 11 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java create mode 100644 src/main/java/com/mongodb/hibernate/dialect/package-info.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/NotYetImplementedException.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/package-info.java create mode 100644 src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java create mode 100644 src/main/java/com/mongodb/hibernate/jdbc/package-info.java create mode 100644 src/test/java/com/mongodb/hibernate/SessionFactoryTests.java create mode 100644 src/test/resources/logback-test.xml diff --git a/.evergreen/static-check.sh b/.evergreen/static-check.sh index 29bf693..8119340 100755 --- a/.evergreen/static-check.sh +++ b/.evergreen/static-check.sh @@ -11,4 +11,4 @@ source java-config.sh echo "mongo-hibernate: static checking ..." ./gradlew -version -./gradlew -PxmlReports.enabled=true --info -x test clean check +./gradlew -PxmlReports.enabled=true --info -x test clean check compileJava diff --git a/build.gradle.kts b/build.gradle.kts index 891550c..5e3fc45 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,30 +17,31 @@ 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) + testImplementation(libs.logback.classic) - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testRuntimeOnly(libs.junit.platform.launcher) errorprone(libs.nullaway) api(libs.jspecify) errorprone(libs.google.errorprone.core) + + implementation(libs.hibernate.core) + implementation(libs.mongo.java.driver.sync) + implementation(libs.sl4j.api) } -// Apply a specific Java toolchain to ease working on different environments. java { toolchain { languageVersion = JavaLanguageVersion.of(17) @@ -48,7 +49,6 @@ java { } tasks.named("test") { - // Use JUnit Platform for unit tests. useJUnitPlatform() } @@ -57,8 +57,7 @@ tasks.named("test") { spotless { java { - // note: you can use an empty string for all the imports you didn't specify explicitly, '|' to join group without blank line, and '\\#` prefix for static imports - importOrder("java|javax", "", "\\#") + importOrder() removeUnusedImports() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe1218a..dcab1ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,19 +2,28 @@ # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format [versions] -junit-jupiter = "5.10.3" +junit-jupiter = "5.11.3" spotless = "6.25.0" palantir = "2.50.0" -errorprone = "4.0.1" -google-errorprone-core = "2.9.0" -nullaway = "0.10.26" -jspecify = "0.3.0" +errorprone = "4.1.0" +google-errorprone-core = "2.35.1" +nullaway = "0.12.1" +jspecify = "1.0.0" +hibernate-core = "6.6.1.Final" +mongo-java-driver-sync = "5.2.0" +slf4j-api = "2.0.16" +logback-classic = "1.5.12" [libraries] junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } 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" } +sl4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-classic" } [plugins] spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java new file mode 100644 index 0000000..3901810 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * A MongoDB {@link Dialect} for {@linkplain #getMinimumSupportedVersion() version 6.0 and above}. + * + *

Usually Hibernate dialect represents some SQL RDBMS and speaks SQL with vendor-specific difference. MongoDB is a + * document DB and speaks MQL (MongoDB Query Language), but it is still possible to integrate with Hibernate by + * creating a JDBC adaptor on top of MongoDB Java + * Driver. + */ +public final class MongoDialect extends Dialect { + private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make(6); + + /** Default constructor used when no version info is available. */ + public MongoDialect() { + super((DatabaseVersion) null); + } + + /** + * Constructor used when MongoDB meta data is available. + * + * @param info MongoDB meta data + */ + public MongoDialect(DialectResolutionInfo info) { + super(info); + } + + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return MINIMUM_VERSION; + } +} diff --git a/src/main/java/com/mongodb/hibernate/dialect/package-info.java b/src/main/java/com/mongodb/hibernate/dialect/package-info.java new file mode 100644 index 0000000..ad71b30 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/dialect/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NullMarked +package com.mongodb.hibernate.dialect; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/mongodb/hibernate/internal/NotYetImplementedException.java b/src/main/java/com/mongodb/hibernate/internal/NotYetImplementedException.java new file mode 100644 index 0000000..848400e --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/NotYetImplementedException.java @@ -0,0 +1,53 @@ +/* + * 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.internal; + +import java.io.Serial; + +/** + * A temporary marker exception to denote that the feature in question is in the scope of MongoDB dialect but has not + * been implemented yet. + * + *

Ultimately all of its references should be eliminated sooner or later, and then this class is supposed to be + * deleted prior to product release. + * + *

It is recommended to provide some message to explain when it will be implemented (e.g. JIRA ticket id is a good + * idea), but that is optional. + * + *

This class is not part of the public API and may be removed or changed at any time. + */ +public final class NotYetImplementedException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Default constructor. + * + *

It is recommended to use the other constructor with some explanation. + */ + public NotYetImplementedException() {} + + /** + * Constructor with message parameter. + * + * @param message explanation on when the feature is to be implemented + */ + public NotYetImplementedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/package-info.java b/src/main/java/com/mongodb/hibernate/internal/package-info.java new file mode 100644 index 0000000..97202b4 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NullMarked +package com.mongodb.hibernate.internal; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java new file mode 100644 index 0000000..21c04d7 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnectionProvider.java @@ -0,0 +1,153 @@ +/* + * 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.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_PASSWORD; +import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_URL; +import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_USER; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.hibernate.internal.NotYetImplementedException; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.sql.Connection; +import java.util.Map; +import org.hibernate.HibernateException; +import org.hibernate.cfg.JdbcSettings; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.service.UnknownUnwrapTypeException; +import org.hibernate.service.spi.Configurable; +import org.hibernate.service.spi.Stoppable; +import org.jspecify.annotations.Nullable; + +/** + * {@linkplain com.mongodb.hibernate.dialect.MongoDialect MongoDB dialect}'s customized JDBC {@link ConnectionProvider} + * SPI implementation. + * + *

{@link MongoConnectionProvider} uses the following Hibernate properties: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertyDescriptionRequired
{@value JdbcSettings#JAKARTA_JDBC_URL}MongoDB + * connection string, + * which must specify the database name for authentication + * if {@value JdbcSettings#JAKARTA_JDBC_USER} is specified.
{@value JdbcSettings#JAKARTA_JDBC_USER}{@code userName} for {@link com.mongodb.MongoCredential#createCredential(String, String, char[])}
{@value JdbcSettings#JAKARTA_JDBC_PASSWORD}{@code password} for {@link com.mongodb.MongoCredential#createCredential(String, String, char[])}
+ * + * @see ConnectionProvider + * @see JdbcSettings#JAKARTA_JDBC_URL + * @see JdbcSettings#JAKARTA_JDBC_USER + * @see JdbcSettings#JAKARTA_JDBC_PASSWORD + * @see connection string + */ +public final class MongoConnectionProvider implements ConnectionProvider, Configurable, Stoppable { + + @Serial + private static final long serialVersionUID = 1L; + + private @Nullable MongoClient mongoClient; + + @Override + public Connection getConnection() { + throw new NotYetImplementedException( + "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-29"); + } + + @Override + public void closeConnection(Connection connection) { + throw new NotYetImplementedException( + "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-29"); + } + + @Override + public boolean supportsAggressiveRelease() { + return false; + } + + @Override + public boolean isUnwrappableAs(Class unwrapType) { + return false; + } + + @Override + public T unwrap(Class unwrapType) { + throw new UnknownUnwrapTypeException(unwrapType); + } + + @Override + public void configure(Map configValues) { + var jdbcUrl = configValues.get(JAKARTA_JDBC_URL); + if (jdbcUrl == null) { + throw new HibernateException("Configuration is required: " + JAKARTA_JDBC_URL); + } + if (!(jdbcUrl instanceof String)) { + throw new HibernateException( + String.format("Configuration [%s] value [%s] not of string type", JAKARTA_JDBC_URL, jdbcUrl)); + } + ConnectionString connectionString; + try { + connectionString = new ConnectionString((String) jdbcUrl); + } catch (RuntimeException e) { + throw new HibernateException( + String.format( + "Failed to create ConnectionString from configuration [%s] with value [%s]", + JAKARTA_JDBC_URL, jdbcUrl), + e); + } + + var clientSettingsBuilder = MongoClientSettings.builder().applyConnectionString(connectionString); + + if (configValues.get(JAKARTA_JDBC_USER) != null || configValues.get(JAKARTA_JDBC_PASSWORD) != null) { + throw new NotYetImplementedException("To be implemented after auth could be tested in CI"); + } + + var clientSettings = clientSettingsBuilder.build(); + this.mongoClient = MongoClients.create(clientSettings); + } + + @Override + public void stop() { + if (this.mongoClient != null) { + this.mongoClient.close(); + } + } + + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + throw new NotSerializableException( + "This class is not designed to be serialized despite it having to implement `Serializable`"); + } +} diff --git a/src/main/java/com/mongodb/hibernate/jdbc/package-info.java b/src/main/java/com/mongodb/hibernate/jdbc/package-info.java new file mode 100644 index 0000000..8c23f99 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NullMarked +package com.mongodb.hibernate.jdbc; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/com/mongodb/hibernate/SessionFactoryTests.java b/src/test/java/com/mongodb/hibernate/SessionFactoryTests.java new file mode 100644 index 0000000..a507045 --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/SessionFactoryTests.java @@ -0,0 +1,64 @@ +/* + * 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; + +import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_URL; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.mongodb.hibernate.dialect.MongoDialect; +import com.mongodb.hibernate.jdbc.MongoConnectionProvider; +import java.util.HashMap; +import java.util.Map; +import org.hibernate.HibernateException; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.service.spi.ServiceException; +import org.junit.jupiter.api.Test; + +class SessionFactoryTests { + + @Test + void testSuccess() { + buildSessionFactory(Map.of(JAKARTA_JDBC_URL, "mongodb://localhost/test")); + } + + @Test + void testInvalidConnectionString() { + var exception = assertThrows( + ServiceException.class, + () -> buildSessionFactory(Map.of(JAKARTA_JDBC_URL, "jdbc:postgresql://localhost/test"))); + assertInstanceOf(HibernateException.class, exception.getCause()); + } + + private void buildSessionFactory(Map jdbcSettings) throws ServiceException { + var settings = new HashMap<>(jdbcSettings); + settings.put(AvailableSettings.DIALECT, MongoDialect.class.getName()); + settings.put(AvailableSettings.CONNECTION_PROVIDER, MongoConnectionProvider.class.getName()); + + var standardServiceRegistry = + new StandardServiceRegistryBuilder().applySettings(settings).build(); + var sessionFactoryBuilder = new MetadataSources(standardServiceRegistry) + .getMetadataBuilder() + .build() + .getSessionFactoryBuilder(); + try (var ignored = sessionFactoryBuilder.build()) { + // no-op + } + } +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..9719b07 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file