From 48b365f64a1cef35cffc627186e7e6ee9df47d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Tue, 6 Jul 2021 17:51:48 +0200 Subject: [PATCH] Liquibase MongoDB extension Fixes #18009, #9801 --- .github/native-tests.json | 2 +- bom/application/pom.xml | 16 + .../java/io/quarkus/deployment/Feature.java | 1 + devtools/bom-descriptor-json/pom.xml | 13 + docs/pom.xml | 13 + .../liquibase-mongodb/deployment/pom.xml | 48 +++ .../deployment/LiquibaseProcessor.java | 325 ++++++++++++++++++ extensions/liquibase-mongodb/pom.xml | 22 ++ extensions/liquibase-mongodb/runtime/pom.xml | 71 ++++ .../liquibase/LiquibaseMongodbFactory.java | 95 +++++ .../LiquibaseMongodbBuildTimeConfig.java | 16 + .../runtime/LiquibaseMongodbConfig.java | 68 ++++ .../runtime/LiquibaseMongodbRecorder.java | 63 ++++ .../runtime/graal/SubstituteStringUtil.java | 37 ++ .../resources/META-INF/quarkus-extension.yaml | 11 + extensions/pom.xml | 1 + integration-tests/liquibase-mongodb/pom.xml | 106 ++++++ .../quarkus/it/liquibase/mongodb/Fruit.java | 8 + .../it/liquibase/mongodb/FruitResource.java | 33 ++ .../src/main/resources/application.properties | 4 + .../main/resources/liquibase/changelog.xml | 23 ++ .../liquibase/mongodb/FruitResourceTest.java | 51 +++ integration-tests/pom.xml | 1 + 23 files changed, 1027 insertions(+), 1 deletion(-) create mode 100644 extensions/liquibase-mongodb/deployment/pom.xml create mode 100644 extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java create mode 100644 extensions/liquibase-mongodb/pom.xml create mode 100644 extensions/liquibase-mongodb/runtime/pom.xml create mode 100644 extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java create mode 100644 extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java create mode 100644 extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java create mode 100644 extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java create mode 100644 extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java create mode 100644 extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 integration-tests/liquibase-mongodb/pom.xml create mode 100644 integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/Fruit.java create mode 100644 integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/FruitResource.java create mode 100644 integration-tests/liquibase-mongodb/src/main/resources/application.properties create mode 100644 integration-tests/liquibase-mongodb/src/main/resources/liquibase/changelog.xml create mode 100644 integration-tests/liquibase-mongodb/src/test/java/io/quarkus/it/liquibase/mongodb/FruitResourceTest.java diff --git a/.github/native-tests.json b/.github/native-tests.json index fb363a4eaa111..e0b017c8e9c6c 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -21,7 +21,7 @@ { "category": "Data3", "timeout": 70, - "test-modules": "flyway, hibernate-orm-panache, hibernate-orm-panache-kotlin, hibernate-orm-envers, liquibase", + "test-modules": "flyway, hibernate-orm-panache, hibernate-orm-panache-kotlin, hibernate-orm-envers, liquibase, liquibase-mongodb", "os-name": "ubuntu-latest" }, { diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 5a28a7cab7d31..ebfe0a06a2c6f 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -159,6 +159,7 @@ 7.14.0 1.0.9 4.4.3 + 4.4.3 1.29 6.0.0 4.3.4 @@ -818,6 +819,16 @@ quarkus-liquibase-deployment ${project.version} + + io.quarkus + quarkus-liquibase-mongodb + ${project.version} + + + io.quarkus + quarkus-liquibase-mongodb-deployment + ${project.version} + io.quarkus quarkus-hibernate-orm @@ -5061,6 +5072,11 @@ + + org.liquibase.ext + liquibase-mongodb + ${liquibase-mongodb.version} + org.yaml snakeyaml diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 6ec6eeb0eba48..4abf8590c92bf 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -62,6 +62,7 @@ public enum Feature { KUBERNETES, KUBERNETES_CLIENT, LIQUIBASE, + LIQUIBASE_MONGODB, LOGGING_GELF, MAILER, MICROMETER, diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index d18fd370ada25..00fe839c0311f 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1332,6 +1332,19 @@ + + io.quarkus + quarkus-liquibase-mongodb + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-logging-gelf diff --git a/docs/pom.xml b/docs/pom.xml index 0b26767dec399..4b4dc0cf1b996 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1293,6 +1293,19 @@ + + io.quarkus + quarkus-liquibase-mongodb-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-logging-gelf-deployment diff --git a/extensions/liquibase-mongodb/deployment/pom.xml b/extensions/liquibase-mongodb/deployment/pom.xml new file mode 100644 index 0000000000000..6cac71b48c6cd --- /dev/null +++ b/extensions/liquibase-mongodb/deployment/pom.xml @@ -0,0 +1,48 @@ + + + + quarkus-liquibase-mongodb-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-liquibase-mongodb-deployment + Quarkus - Liquibase MongoDB - Deployment + + + + io.quarkus + quarkus-liquibase-mongodb + + + io.quarkus + quarkus-mongodb-client-deployment + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java b/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java new file mode 100644 index 0000000000000..ec9ddc2a910de --- /dev/null +++ b/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java @@ -0,0 +1,325 @@ +package io.quarkus.liquibase.mongodb.deployment; + +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.logging.Logger; + +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.liquibase.LiquibaseMongodbFactory; +import io.quarkus.liquibase.runtime.LiquibaseMongodbBuildTimeConfig; +import io.quarkus.liquibase.runtime.LiquibaseMongodbConfig; +import io.quarkus.liquibase.runtime.LiquibaseMongodbRecorder; +import io.quarkus.mongodb.runtime.MongodbConfig; +import liquibase.change.Change; +import liquibase.change.DatabaseChangeProperty; +import liquibase.change.core.CreateProcedureChange; +import liquibase.change.core.CreateViewChange; +import liquibase.change.core.LoadDataChange; +import liquibase.change.core.SQLFileChange; +import liquibase.changelog.ChangeLogParameters; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.exception.LiquibaseException; +import liquibase.parser.ChangeLogParser; +import liquibase.parser.ChangeLogParserFactory; +import liquibase.resource.ClassLoaderResourceAccessor; + +class LiquibaseProcessor { + + private static final Logger LOGGER = Logger.getLogger(LiquibaseProcessor.class); + + private static final String LIQUIBASE_BEAN_NAME_PREFIX = "liquibase_"; + + private static final DotName DATABASE_CHANGE_PROPERTY = DotName.createSimple(DatabaseChangeProperty.class.getName()); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.LIQUIBASE_MONGODB); + } + + @BuildStep + public SystemPropertyBuildItem disableHub() { + // Don't block app startup with prompt: + // Do you want to see this operation's report in Liquibase Hub, which improves team collaboration? + // If so, enter your email. If not, enter [N] to no longer be prompted, or [S] to skip for now, but ask again next time (default "S"): + return new SystemPropertyBuildItem("liquibase.hub.mode", "off"); + } + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + @Record(STATIC_INIT) + void nativeImageConfiguration( + LiquibaseMongodbRecorder recorder, + LiquibaseMongodbBuildTimeConfig liquibaseBuildConfig, + CombinedIndexBuildItem combinedIndex, + BuildProducer reflective, + BuildProducer resource, + BuildProducer services, + BuildProducer runtimeInitialized, + BuildProducer resourceBundle) { + + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem(liquibase.diff.compare.CompareControl.class.getName())); + + reflective.produce(new ReflectiveClassBuildItem(false, true, false, + liquibase.change.AbstractSQLChange.class.getName(), + liquibase.database.jvm.JdbcConnection.class.getName())); + + reflective.produce(new ReflectiveClassBuildItem(true, true, true, + liquibase.parser.ChangeLogParserCofiguration.class.getName(), + liquibase.hub.HubServiceFactory.class.getName(), + liquibase.logging.core.DefaultLoggerConfiguration.class.getName(), + liquibase.configuration.GlobalConfiguration.class.getName(), + com.datical.liquibase.ext.config.LiquibaseProConfiguration.class.getName(), + liquibase.license.LicenseServiceFactory.class.getName(), + liquibase.executor.ExecutorService.class.getName(), + liquibase.change.ChangeFactory.class.getName(), + liquibase.logging.core.LogServiceFactory.class.getName(), + liquibase.logging.LogFactory.class.getName(), + liquibase.change.ColumnConfig.class.getName(), + liquibase.change.AddColumnConfig.class.getName(), + liquibase.change.core.LoadDataColumnConfig.class.getName(), + liquibase.sql.visitor.PrependSqlVisitor.class.getName(), + liquibase.sql.visitor.ReplaceSqlVisitor.class.getName(), + liquibase.sql.visitor.AppendSqlVisitor.class.getName(), + liquibase.sql.visitor.RegExpReplaceSqlVisitor.class.getName())); + + reflective.produce(new ReflectiveClassBuildItem(false, false, true, + liquibase.change.ConstraintsConfig.class.getName())); + + // register classes marked with @DatabaseChangeProperty for reflection + Set classesMarkedWithDatabaseChangeProperty = new HashSet<>(); + for (AnnotationInstance databaseChangePropertyInstance : combinedIndex.getIndex() + .getAnnotations(DATABASE_CHANGE_PROPERTY)) { + // the annotation is only supported on methods but let's be safe + AnnotationTarget annotationTarget = databaseChangePropertyInstance.target(); + if (annotationTarget.kind() == AnnotationTarget.Kind.METHOD) { + classesMarkedWithDatabaseChangeProperty.add(annotationTarget.asMethod().declaringClass().name().toString()); + } + } + reflective.produce( + new ReflectiveClassBuildItem(true, true, true, classesMarkedWithDatabaseChangeProperty.toArray(new String[0]))); + + resource.produce( + new NativeImageResourceBuildItem(getChangeLogs(liquibaseBuildConfig).toArray(new String[0]))); + + Stream.of(liquibase.change.Change.class, + liquibase.changelog.ChangeLogHistoryService.class, + liquibase.command.LiquibaseCommand.class, + liquibase.database.Database.class, + liquibase.database.DatabaseConnection.class, + liquibase.datatype.LiquibaseDataType.class, + liquibase.diff.compare.DatabaseObjectComparator.class, + liquibase.diff.DiffGenerator.class, + liquibase.diff.output.changelog.ChangeGenerator.class, + liquibase.executor.Executor.class, + liquibase.license.LicenseService.class, + liquibase.lockservice.LockService.class, + liquibase.logging.LogService.class, + liquibase.parser.ChangeLogParser.class, + liquibase.parser.NamespaceDetails.class, + liquibase.parser.SnapshotParser.class, + liquibase.precondition.Precondition.class, + liquibase.serializer.ChangeLogSerializer.class, + liquibase.serializer.SnapshotSerializer.class, + liquibase.servicelocator.ServiceLocator.class, + liquibase.snapshot.SnapshotGenerator.class, + liquibase.sqlgenerator.SqlGenerator.class, + liquibase.structure.DatabaseObject.class, + liquibase.hub.HubService.class) + .forEach(t -> addService(services, reflective, t, false)); + + // Register Precondition services, and the implementation class for reflection while also registering fields for reflection + addService(services, reflective, liquibase.precondition.Precondition.class, true); + + // liquibase XSD + resource.produce(new NativeImageResourceBuildItem( + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.7.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.9.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.10.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.0.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-3.8.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-3.9.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-3.10.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-4.0.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-4.1.xsd", + "liquibase.build.properties")); + + // liquibase resource bundles + resourceBundle.produce(new NativeImageResourceBundleBuildItem("liquibase/i18n/liquibase-core")); + } + + private void addService(BuildProducer services, + BuildProducer reflective, Class serviceClass, + boolean shouldRegisterFieldForReflection) { + try { + String service = "META-INF/services/" + serviceClass.getName(); + Set implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), + service); + services.produce(new ServiceProviderBuildItem(serviceClass.getName(), implementations.toArray(new String[0]))); + + reflective.produce(new ReflectiveClassBuildItem(true, true, shouldRegisterFieldForReflection, + implementations.toArray(new String[0]))); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void createBeans(LiquibaseMongodbRecorder recorder, + LiquibaseMongodbConfig liquibaseMongodbConfig, + LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig, + MongodbConfig mongodbConfig, + BuildProducer syntheticBeanBuildItemBuildProducer) { + + SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(LiquibaseMongodbFactory.class) + .scope(ApplicationScoped.class) // this is what the existing code does, but it doesn't seem reasonable + .setRuntimeInit() + .unremovable() + .supplier(recorder.liquibaseSupplier(liquibaseMongodbConfig, liquibaseMongodbBuildTimeConfig, mongodbConfig)); + + syntheticBeanBuildItemBuildProducer.produce(configurator.done()); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + @Consume(SyntheticBeansRuntimeInitBuildItem.class) + ServiceStartBuildItem startLiquibase(LiquibaseMongodbRecorder recorder) { + // will actually run the actions at runtime + recorder.doStartActions(); + + return new ServiceStartBuildItem("liquibase-mongodb"); + } + + /** + * Collect the configured changeLog file for the default and all named datasources. + *

+ * A {@link LinkedHashSet} is used to avoid duplications. + */ + private List getChangeLogs(LiquibaseMongodbBuildTimeConfig liquibaseBuildConfig) { + ChangeLogParameters changeLogParameters = new ChangeLogParameters(); + ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor( + Thread.currentThread().getContextClassLoader()); + + ChangeLogParserFactory changeLogParserFactory = ChangeLogParserFactory.getInstance(); + + Set resources = new LinkedHashSet<>(); + + resources.addAll(findAllChangeLogFiles(liquibaseBuildConfig.changeLog, changeLogParserFactory, + classLoaderResourceAccessor, changeLogParameters)); + + LOGGER.debugf("Liquibase changeLogs: %s", resources); + + return new ArrayList<>(resources); + } + + /** + * Finds all resource files for the given change log file + */ + private Set findAllChangeLogFiles(String file, ChangeLogParserFactory changeLogParserFactory, + ClassLoaderResourceAccessor classLoaderResourceAccessor, + ChangeLogParameters changeLogParameters) { + try { + ChangeLogParser parser = changeLogParserFactory.getParser(file, classLoaderResourceAccessor); + DatabaseChangeLog changelog = parser.parse(file, changeLogParameters, classLoaderResourceAccessor); + + if (changelog != null) { + Set result = new LinkedHashSet<>(); + // get all changeSet files + for (ChangeSet changeSet : changelog.getChangeSets()) { + result.add(changeSet.getFilePath()); + + changeSet.getChanges().stream() + .map(change -> extractChangeFile(change, changeSet.getFilePath())) + .forEach(changeFile -> changeFile.ifPresent(result::add)); + + // get all parents of the changeSet + DatabaseChangeLog parent = changeSet.getChangeLog(); + while (parent != null) { + result.add(parent.getFilePath()); + parent = parent.getParentChangeLog(); + } + } + result.add(changelog.getFilePath()); + return result; + } + } catch (LiquibaseException ex) { + throw new IllegalStateException(ex); + } + return Collections.emptySet(); + } + + private Optional extractChangeFile(Change change, String changeSetFilePath) { + String path = null; + Boolean relative = null; + if (change instanceof LoadDataChange) { + LoadDataChange loadDataChange = (LoadDataChange) change; + path = loadDataChange.getFile(); + relative = loadDataChange.isRelativeToChangelogFile(); + } else if (change instanceof SQLFileChange) { + SQLFileChange sqlFileChange = (SQLFileChange) change; + path = sqlFileChange.getPath(); + relative = sqlFileChange.isRelativeToChangelogFile(); + } else if (change instanceof CreateProcedureChange) { + CreateProcedureChange createProcedureChange = (CreateProcedureChange) change; + path = createProcedureChange.getPath(); + relative = createProcedureChange.isRelativeToChangelogFile(); + } else if (change instanceof CreateViewChange) { + CreateViewChange createViewChange = (CreateViewChange) change; + path = createViewChange.getPath(); + relative = createViewChange.getRelativeToChangelogFile(); + } + + // unrelated change or change does not reference a file (e.g. inline view) + if (path == null) { + return Optional.empty(); + } + // absolute file path or changeSet has no file path + if (relative == null || !relative || changeSetFilePath == null) { + return Optional.of(path); + } + + // relative file path needs to be resolved against changeSetFilePath + // notes: ClassLoaderResourceAccessor does not provide a suitable method and CLRA.getFinalPath() is not visible + return Optional.of(Paths.get(changeSetFilePath).resolveSibling(path).toString().replace('\\', '/')); + } +} diff --git a/extensions/liquibase-mongodb/pom.xml b/extensions/liquibase-mongodb/pom.xml new file mode 100644 index 0000000000000..f9432f43440cb --- /dev/null +++ b/extensions/liquibase-mongodb/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-liquibase-mongodb-parent + Quarkus - Liquibase MongoDB + pom + + + runtime + deployment + + + \ No newline at end of file diff --git a/extensions/liquibase-mongodb/runtime/pom.xml b/extensions/liquibase-mongodb/runtime/pom.xml new file mode 100644 index 0000000000000..e027988136da5 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/pom.xml @@ -0,0 +1,71 @@ + + + + quarkus-liquibase-mongodb-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-liquibase-mongodb + Quarkus - Liquibase MongoDB - Runtime + Handle your MongoDB schema migrations with Liquibase + + + io.quarkus + quarkus-mongodb-client + + + org.liquibase + liquibase-core + + + org.yaml + snakeyaml + + + org.liquibase.ext + liquibase-mongodb + + + org.graalvm.nativeimage + svm + provided + + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.liquibase.mongodb + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java new file mode 100644 index 0000000000000..5369cfdad6b3b --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java @@ -0,0 +1,95 @@ +package io.quarkus.liquibase; + +import java.util.Map; +import java.util.regex.Pattern; + +import io.quarkus.liquibase.runtime.LiquibaseMongodbBuildTimeConfig; +import io.quarkus.liquibase.runtime.LiquibaseMongodbConfig; +import io.quarkus.mongodb.runtime.MongoClientConfig; +import liquibase.Contexts; +import liquibase.LabelExpression; +import liquibase.Liquibase; +import liquibase.database.Database; +import liquibase.database.DatabaseFactory; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.resource.ResourceAccessor; + +public class LiquibaseMongodbFactory { + + private final MongoClientConfig mongoClientConfig; + private final LiquibaseMongodbConfig liquibaseMongodbConfig; + private final LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig; + + //connection-string format, see https://docs.mongodb.com/manual/reference/connection-string/ + Pattern HAS_DB = Pattern.compile("(mongodb|mongodb\\+srv)://[^/]*/.*"); + + public LiquibaseMongodbFactory(LiquibaseMongodbConfig config, + LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig, MongoClientConfig mongoClientConfig) { + this.liquibaseMongodbConfig = config; + this.liquibaseMongodbBuildTimeConfig = liquibaseMongodbBuildTimeConfig; + this.mongoClientConfig = mongoClientConfig; + } + + public Liquibase createLiquibase() { + try { + ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader()); + String connectionString = this.mongoClientConfig.connectionString.orElse("mongodb://localhost:27017"); + if (!HAS_DB.matcher(connectionString).matches()) { + connectionString += "/" + this.mongoClientConfig.database.orElseThrow( + () -> new IllegalArgumentException( + "Config property 'quarkus.mongodb.database' must be defined when no database " + + "exist in the connection string")); + } + Database database = DatabaseFactory.getInstance().openDatabase(connectionString, + this.mongoClientConfig.credentials.username.orElse(null), + this.mongoClientConfig.credentials.password.orElse(null), + null, resourceAccessor); + + ; + if (database != null) { + liquibaseMongodbConfig.liquibaseCatalogName.ifPresent(database::setLiquibaseCatalogName); + liquibaseMongodbConfig.liquibaseSchemaName.ifPresent(database::setLiquibaseSchemaName); + liquibaseMongodbConfig.liquibaseTablespaceName.ifPresent(database::setLiquibaseTablespaceName); + + if (liquibaseMongodbConfig.defaultCatalogName.isPresent()) { + database.setDefaultCatalogName(liquibaseMongodbConfig.defaultCatalogName.get()); + } + if (liquibaseMongodbConfig.defaultSchemaName.isPresent()) { + database.setDefaultSchemaName(liquibaseMongodbConfig.defaultSchemaName.get()); + } + } + Liquibase liquibase = new Liquibase(liquibaseMongodbBuildTimeConfig.changeLog, resourceAccessor, database); + + for (Map.Entry entry : liquibaseMongodbConfig.changeLogParameters.entrySet()) { + liquibase.getChangeLogParameters().set(entry.getKey(), entry.getValue()); + } + + return liquibase; + + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + public LiquibaseMongodbConfig getConfiguration() { + return liquibaseMongodbConfig; + } + + /** + * Creates the default labels base on the configuration + * + * @return the label expression + */ + public LabelExpression createLabels() { + return new LabelExpression(liquibaseMongodbConfig.labels.orElse(null)); + } + + /** + * Creates the default contexts base on the configuration + * + * @return the contexts + */ + public Contexts createContexts() { + return new Contexts(liquibaseMongodbConfig.contexts.orElse(null)); + } +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java new file mode 100644 index 0000000000000..9bd5432a0f75e --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.liquibase.runtime; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * The liquibase configuration + */ +@ConfigRoot(name = "liquibase-mongodb", phase = ConfigPhase.BUILD_TIME) +public class LiquibaseMongodbBuildTimeConfig { + + /** + * The change log file + */ + public String changeLog = "db/changeLog.xml"; +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java new file mode 100644 index 0000000000000..6cab1009c19d7 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java @@ -0,0 +1,68 @@ +package io.quarkus.liquibase.runtime; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * The liquibase configuration + */ +@ConfigRoot(name = "liquibase-mongodb", phase = ConfigPhase.RUN_TIME) +public class LiquibaseMongodbConfig { + + /** + * The migrate at start flag + */ + public boolean migrateAtStart = false; + + /** + * The validate on update flag + */ + public boolean validateOnMigrate = true; + + /** + * The clean at start flag + */ + public boolean cleanAtStart = false; + + public Map changeLogParameters = null; + + /** + * The list of contexts + */ + public Optional> contexts = null; + + /** + * The list of labels + */ + public Optional> labels = null; + + /** + * The default catalog name + */ + public Optional defaultCatalogName = Optional.empty(); + + /** + * The default schema name + */ + public Optional defaultSchemaName = Optional.empty(); + + /** + * The liquibase tables catalog name + */ + public Optional liquibaseCatalogName = Optional.empty(); + + /** + * The liquibase tables schema name + */ + public Optional liquibaseSchemaName = Optional.empty(); + + /** + * The liquibase tables tablespace name + */ + public Optional liquibaseTablespaceName = Optional.empty(); + +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java new file mode 100644 index 0000000000000..84759f8ac1e6c --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java @@ -0,0 +1,63 @@ +package io.quarkus.liquibase.runtime; + +import java.util.function.Supplier; + +import javax.enterprise.inject.Any; +import javax.enterprise.inject.UnsatisfiedResolutionException; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.liquibase.LiquibaseMongodbFactory; +import io.quarkus.mongodb.runtime.MongodbConfig; +import io.quarkus.runtime.annotations.Recorder; +import liquibase.Liquibase; + +@Recorder +public class LiquibaseMongodbRecorder { + + public Supplier liquibaseSupplier(LiquibaseMongodbConfig config, + LiquibaseMongodbBuildTimeConfig buildTimeConfig, MongodbConfig mongodbConfig) { + return new Supplier() { + @Override + public LiquibaseMongodbFactory get() { + return new LiquibaseMongodbFactory(config, buildTimeConfig, mongodbConfig.defaultMongoClientConfig); + } + }; + } + + public void doStartActions() { + try { + InjectableInstance liquibaseFactoryInstance = Arc.container() + .select(LiquibaseMongodbFactory.class, Any.Literal.INSTANCE); + if (liquibaseFactoryInstance.isUnsatisfied()) { + return; + } + + for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { + try { + LiquibaseMongodbFactory liquibaseFactory = liquibaseFactoryHandle.get(); + if (liquibaseFactory.getConfiguration().cleanAtStart) { + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.dropAll(); + } + } + if (liquibaseFactory.getConfiguration().migrateAtStart) { + if (liquibaseFactory.getConfiguration().validateOnMigrate) { + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.validate(); + } + } + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } + } + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured + } + } + } catch (Exception e) { + throw new IllegalStateException("Error starting Liquibase", e); + } + } +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java new file mode 100644 index 0000000000000..d4f28a00e8464 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java @@ -0,0 +1,37 @@ +package io.quarkus.liquibase.runtime.graal; + +import java.security.SecureRandom; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.InjectAccessors; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "liquibase.util.StringUtil") +final class SubstituteStringUtil { + + @Alias + @InjectAccessors(SecureRandomAccessors.class) + private static SecureRandom rnd; + + public static final class SecureRandomAccessors { + + private static volatile SecureRandom volatileRandom; + + public static SecureRandom get() { + SecureRandom localVolatileRandom = volatileRandom; + if (localVolatileRandom == null) { + synchronized (SecureRandomAccessors.class) { + localVolatileRandom = volatileRandom; + if (localVolatileRandom == null) { + volatileRandom = localVolatileRandom = new SecureRandom(); + } + } + } + return localVolatileRandom; + } + + public static void set(SecureRandom rnd) { + throw new IllegalStateException("The setter for liquibase.util.StringUtil#rnd shouldn't be called."); + } + } +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..8cacc679b7975 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Liquibase MongoDB" +metadata: + keywords: + - "liquibase" + - "mongodb" + - "data" + categories: + - "data" + status: "experimental" \ No newline at end of file diff --git a/extensions/pom.xml b/extensions/pom.xml index 03e4923b78cb8..d20956728e9ea 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -175,6 +175,7 @@ flyway liquibase + liquibase-mongodb vault diff --git a/integration-tests/liquibase-mongodb/pom.xml b/integration-tests/liquibase-mongodb/pom.xml new file mode 100644 index 0000000000000..dc1d8393d2d02 --- /dev/null +++ b/integration-tests/liquibase-mongodb/pom.xml @@ -0,0 +1,106 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-liquibase-mongodb + Quarkus - Integration Tests - Liquibase MongoDB + Module that contains Liquibase MongoDB related tests + + + + io.quarkus + quarkus-liquibase-mongodb + + + io.quarkus + quarkus-mongodb-panache + + + io.quarkus + quarkus-resteasy-jackson + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-mongodb + test + + + + + io.quarkus + quarkus-liquibase-mongodb-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-mongodb-panache-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/Fruit.java b/integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/Fruit.java new file mode 100644 index 0000000000000..b5d0483404c0e --- /dev/null +++ b/integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/Fruit.java @@ -0,0 +1,8 @@ +package io.quarkus.it.liquibase.mongodb; + +import io.quarkus.mongodb.panache.PanacheMongoEntity; + +public class Fruit extends PanacheMongoEntity { + public String name; + public String color; +} diff --git a/integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/FruitResource.java b/integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/FruitResource.java new file mode 100644 index 0000000000000..cdbb0a4efc69c --- /dev/null +++ b/integration-tests/liquibase-mongodb/src/main/java/io/quarkus/it/liquibase/mongodb/FruitResource.java @@ -0,0 +1,33 @@ +package io.quarkus.it.liquibase.mongodb; + +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.bson.types.ObjectId; + +@Path("/fruits") +@Produces(MediaType.APPLICATION_JSON) +public class FruitResource { + + @GET + public List list() { + return Fruit.listAll(); + } + + @GET + @Path("/{id}") + public Fruit get(@PathParam("id") String id) { + return Fruit.findById(new ObjectId(id)); + } + + @POST + public void save(Fruit fruit) { + fruit.persist(); + } +} diff --git a/integration-tests/liquibase-mongodb/src/main/resources/application.properties b/integration-tests/liquibase-mongodb/src/main/resources/application.properties new file mode 100644 index 0000000000000..9a40534908c6f --- /dev/null +++ b/integration-tests/liquibase-mongodb/src/main/resources/application.properties @@ -0,0 +1,4 @@ +quarkus.mongodb.connection-string=mongodb://localhost:27017 +quarkus.mongodb.database=fruits +quarkus.liquibase-mongodb.change-log=liquibase/changelog.xml +quarkus.liquibase-mongodb.migrate-at-start=true \ No newline at end of file diff --git a/integration-tests/liquibase-mongodb/src/main/resources/liquibase/changelog.xml b/integration-tests/liquibase-mongodb/src/main/resources/liquibase/changelog.xml new file mode 100644 index 0000000000000..18bd43e5611b4 --- /dev/null +++ b/integration-tests/liquibase-mongodb/src/main/resources/liquibase/changelog.xml @@ -0,0 +1,23 @@ + + + + + + + + {color: 1} + {name: "colorIdx"} + + + + {"name":"orange", "color": "orange"} + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase-mongodb/src/test/java/io/quarkus/it/liquibase/mongodb/FruitResourceTest.java b/integration-tests/liquibase-mongodb/src/test/java/io/quarkus/it/liquibase/mongodb/FruitResourceTest.java new file mode 100644 index 0000000000000..6f9a203d3ec4f --- /dev/null +++ b/integration-tests/liquibase-mongodb/src/test/java/io/quarkus/it/liquibase/mongodb/FruitResourceTest.java @@ -0,0 +1,51 @@ +package io.quarkus.it.liquibase.mongodb; + +import static io.restassured.RestAssured.get; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.inject.Inject; + +import org.bson.Document; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import com.mongodb.client.ListIndexesIterable; +import com.mongodb.client.MongoClient; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.mongodb.MongoTestResource; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +@QuarkusTestResource(MongoTestResource.class) +@DisabledOnOs(OS.WINDOWS) +class FruitResourceTest { + + @Inject + MongoClient mongoClient; + + @Test + public void testTheEndpoint() { + // assert that a fruit exist as one has been created in the changelog + List list = get("/fruits").as(new TypeRef>() { + }); + Assertions.assertEquals(1, list.size()); + } + + @Test + public void validateTheIdx() { + // check that the index that the changelog created exist + ListIndexesIterable indexes = mongoClient.getDatabase("fruits").getCollection("Fruit").listIndexes(); + Set names = StreamSupport.stream(indexes.spliterator(), false) + .map(doc -> doc.getString("name")) + .collect(Collectors.toSet()); + Assertions.assertTrue(names.contains("colorIdx")); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 85cd285c932c9..fb683ab4e1782 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -180,6 +180,7 @@ elytron-undertow flyway liquibase + liquibase-mongodb oidc oidc-client oidc-client-reactive