From 906907adcedcf5d444a214dcd4a91a0249f59986 Mon Sep 17 00:00:00 2001 From: Tomas Hofman Date: Fri, 23 Apr 2021 16:28:31 +0200 Subject: [PATCH] WIP - draft of DevUI pages for Hibernate ORM * provide information about persistence units and their managed entities * allow user to generate create-schema script --- extensions/hibernate-orm/deployment/pom.xml | 4 ++ .../devconsole/DevConsoleProcessor.java | 42 ++++++++++++++++++ .../resources/dev-templates/embedded.html | 11 +++++ .../dev-templates/persistence-units.html | 43 +++++++++++++++++++ .../FastBootHibernatePersistenceProvider.java | 27 ++++++++++++ .../orm/runtime/HibernateOrmRecorder.java | 6 +++ ...ersistenceUnitSchemaGenerationHandler.java | 41 ++++++++++++++++++ .../orm/runtime/PersistenceUnitsHolder.java | 8 +++- .../FastBootEntityManagerFactoryBuilder.java | 37 ++++++++++++++++ .../PreconfiguredServiceRegistryBuilder.java | 8 +++- 10 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/devconsole/DevConsoleProcessor.java create mode 100644 extensions/hibernate-orm/deployment/src/main/resources/dev-templates/embedded.html create mode 100644 extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html create mode 100644 extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitSchemaGenerationHandler.java diff --git a/extensions/hibernate-orm/deployment/pom.xml b/extensions/hibernate-orm/deployment/pom.xml index 8e60cdce048ba..c59b39576e50c 100644 --- a/extensions/hibernate-orm/deployment/pom.xml +++ b/extensions/hibernate-orm/deployment/pom.xml @@ -41,6 +41,10 @@ io.quarkus quarkus-panache-hibernate-common-deployment + + io.quarkus + quarkus-vertx-http-deployment + io.quarkus quarkus-junit5-internal diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/devconsole/DevConsoleProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/devconsole/DevConsoleProcessor.java new file mode 100644 index 0000000000000..edaed88327784 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/devconsole/DevConsoleProcessor.java @@ -0,0 +1,42 @@ +package io.quarkus.hibernate.orm.deployment.devconsole; + +import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem; +import io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; + +import java.util.List; + +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +public class DevConsoleProcessor { + + /** + * Collect a list of available persistence units for the DevUI templates. + */ + @BuildStep(onlyIf = IsDevelopment.class) + public DevConsoleTemplateInfoBuildItem collectDeploymentUnits(List persistenceUnits) { + return new DevConsoleTemplateInfoBuildItem("persistenceUnits", persistenceUnits); + } + + /** + * Register an endpoint that generates create-schema script for given PU. + */ + @BuildStep(onlyIf = IsDevelopment.class) + @Record(RUNTIME_INIT) + @Consume(SyntheticBeansRuntimeInitBuildItem.class) + public RouteBuildItem myExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + HibernateOrmRecorder hibernateOrmRecorder) { + return nonApplicationRootPathBuildItem.routeBuilder() + .route("pu-schema/:name") + .handler(hibernateOrmRecorder.schemaGenerationHandler()) + .build(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/embedded.html b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/embedded.html new file mode 100644 index 0000000000000..f646f630d4e2b --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/embedded.html @@ -0,0 +1,11 @@ +{! TODO: list available persistence units, add links to a detail page and to a page showing create-schema script for each PU !} + + + + Persistence units # {info:persistenceUnits.size} + + +{#for unit in info:persistenceUnits} + + {unit.persistenceUnitName} +{/for} diff --git a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html new file mode 100644 index 0000000000000..36d1fc97b49c6 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html @@ -0,0 +1,43 @@ +{! TODO: a temporary page which shows list of all PUs and a list of all managed entities in each PU. !} + +{#include main} +{#style} +.annotation { +color: gray; +} +{/style} +{#title}Persistence Units{/title} +{#body} + +{#if info:persistenceUnits.isEmpty} +

No persistence units found.

+{#else} +

Count: {info:persistenceUnits}

+ + + + + + + + + {#for unit in info:persistenceUnits} + + + + + {/for} + +
Persistence Unit NameSize
+ {unit.persistenceUnitName} + +
    + {#for cls in unit.managedClassNames} +
  • {cls}
  • + {/for} +
+
+{/if} + +{/body} +{/include} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java index aba131e7684a6..5260d0cdd37e9 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java @@ -1,5 +1,6 @@ package io.quarkus.hibernate.orm.runtime; +import java.io.StringWriter; import java.util.Collections; import java.util.List; import java.util.Map; @@ -17,6 +18,7 @@ import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder; import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; import org.hibernate.service.internal.ProvidedService; +import org.hibernate.tool.hbm2ddl.SchemaExport; import org.jboss.logging.Logger; import io.quarkus.agroal.DataSource.DataSourceLiteral; @@ -99,6 +101,31 @@ public boolean generateSchema(String persistenceUnitName, Map map) { return true; } + /** + * TODO: Figure out how to provide separate "create" and "drop" scripts. Currently this only do "create". + * + * A variant of {@link #generateSchema(String persistenceUnitName, Map map)} that returns the SQL script as a string. + * + * @param persistenceUnitName PU name + * @param map should be always null, as Quarkus doesn't allow to provide any properties in runtime. + * @return schema as string + */ + @SuppressWarnings("rawtypes") + public String generateSchemaToString(String persistenceUnitName, Map map) { + SchemaExport schemaExport = new SchemaExport(); + schemaExport.setFormat(true); + + final FastBootEntityManagerFactoryBuilder builder = (FastBootEntityManagerFactoryBuilder) + getEntityManagerFactoryBuilderOrNull(persistenceUnitName, map); + if (builder == null) { + log.trace("Could not obtain matching EntityManagerFactoryBuilder, returning null"); + return null; + } + StringWriter createWriter = new StringWriter(); + builder.generateSchema(createWriter); + return createWriter.toString(); + } + @Override public ProviderUtil getProviderUtil() { return providerUtil; diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java index d67bc84a504f6..0237dab48c7a4 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java @@ -23,6 +23,8 @@ import io.quarkus.hibernate.orm.runtime.session.ForwardingSession; import io.quarkus.hibernate.orm.runtime.tenant.DataSourceTenantConnectionResolver; import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; /** * @author Emmanuel Bernard emmanuel@hibernate.org @@ -119,4 +121,8 @@ protected Session delegate() { }; } + public Handler schemaGenerationHandler() { + return new PersistenceUnitSchemaGenerationHandler(); + } + } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitSchemaGenerationHandler.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitSchemaGenerationHandler.java new file mode 100644 index 0000000000000..8b08f535855a7 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitSchemaGenerationHandler.java @@ -0,0 +1,41 @@ +package io.quarkus.hibernate.orm.runtime; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; + +import javax.persistence.spi.PersistenceProviderResolverHolder; + +/** + * This handler is used in DevUI to generate a create-schema script for given PU. + */ +public class PersistenceUnitSchemaGenerationHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + HttpServerResponse response = routingContext.response(); + response.setChunked(true); + response.putHeader("content-type", "text/plain"); + + String name = routingContext.pathParam("name"); + + if (name == null || name.isEmpty()) { + for (PersistenceUnitDescriptor descriptor : PersistenceUnitsHolder.getPersistenceUnitDescriptors()) { + response.write(descriptor.getName()).write("\n"); + } + } else { + FastBootHibernatePersistenceProvider persistenceProvider = (FastBootHibernatePersistenceProvider) + PersistenceProviderResolverHolder.getPersistenceProviderResolver().getPersistenceProviders().get(0); + + String schema = persistenceProvider.generateSchemaToString(name, null); + if (schema == null) { + response.setStatusCode(HttpResponseStatus.NOT_FOUND.code()); + response.write("Persistence unit '" + name + "' not found."); + } else { + response.write(schema); + } + } + response.end(); + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitsHolder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitsHolder.java index fb9ba1c76f156..a2c7159ecacc0 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitsHolder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitsHolder.java @@ -54,7 +54,13 @@ public static RecordedState popRecordedState(String persistenceUnitName) { if (persistenceUnitName == null) { key = NO_NAME_TOKEN; } - return persistenceUnits.recordedStates.remove(key); + // FIXME: Changed from remove() to get() to enable DevUI to generate schema scripts. + // The "pop" behavior makes it impossible to construct persistence provider repeatedly (which is needed + // in order to execute `persistenceProvider.generateSchema()`). + // The reason why the recorded state is removed here is to clear away the PU metadata after + // the boot process, when they are no longer needed. + // Would it be OK not to remove the metadata during the dev-mode execution? + return persistenceUnits.recordedStates.get(key); } private static List convertPersistenceUnits( diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java index 7490a4067dc6d..2ae078196f31d 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java @@ -1,7 +1,9 @@ package io.quarkus.hibernate.orm.runtime.boot; import java.io.Serializable; +import java.io.Writer; import java.security.NoSuchAlgorithmException; +import java.util.EnumSet; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityNotFoundException; @@ -24,9 +26,14 @@ import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.hibernate.tool.hbm2ddl.SchemaExport; +import org.hibernate.tool.schema.TargetType; +import org.hibernate.tool.schema.internal.exec.ScriptTargetOutputToWriter; import org.hibernate.tool.schema.spi.CommandAcceptanceException; import org.hibernate.tool.schema.spi.DelayedDropRegistryNotAvailableImpl; import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator; +import org.hibernate.tool.schema.spi.ScriptTargetOutput; +import org.hibernate.tool.schema.spi.TargetDescriptor; import io.quarkus.hibernate.orm.runtime.RuntimeSettings; import io.quarkus.hibernate.orm.runtime.recording.PrevalidatedQuarkusMetadata; @@ -94,6 +101,36 @@ public void generateSchema() { cancel(); } + /** + * TODO: provide a second writer parameter for "drop" script, or create a second method. + * + * @param createWriter + */ + public void generateSchema(final Writer createWriter) { + try { + SchemaExport schemaExport = new SchemaExport(); + schemaExport.setFormat(true); + schemaExport.doExecution(SchemaExport.Action.CREATE, false, metadata, standardServiceRegistry, new TargetDescriptor() { + + @Override + public EnumSet getTargetTypes() { + return EnumSet.of(TargetType.SCRIPT); + } + + @Override + public ScriptTargetOutput getScriptTargetOutput() { + return new ScriptTargetOutputToWriter(createWriter); + } + }); + + } catch (Exception e) { + throw persistenceException("Error performing schema management", e); + } + + // release this builder + cancel(); + } + protected PersistenceException persistenceException(String message, Exception cause) { // Provide a comprehensible message if there is an issue with SSL support Throwable t = cause; diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java index c90ac314b263a..065a0a9b2c777 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java @@ -106,7 +106,13 @@ public StandardServiceRegistryImpl buildNewServiceRegistry() { final Map settingsCopy = new HashMap(); settingsCopy.putAll(configurationValues); - destroyedRegistry.resetAndReactivate(bootstrapServiceRegistry, initiators, providedServices, settingsCopy); + // FIXME: resetAndReactivate() throws "IllegalStateException: Can't reactivate an active registry!" + // during persistenceProvider.generateSchema() execution (a new PersistenceProvider instance is constructed + // during this call). + // Is it OK to skip the resetAndReactivate() call when the registry is already active? + if (!destroyedRegistry.isActive()) { + destroyedRegistry.resetAndReactivate(bootstrapServiceRegistry, initiators, providedServices, settingsCopy); + } return destroyedRegistry; // return new StandardServiceRegistryImpl(