Skip to content

Commit

Permalink
WIP - draft of DevUI pages for Hibernate ORM
Browse files Browse the repository at this point in the history
* provide information about persistence units and their managed entities
* allow user to generate create-schema script
  • Loading branch information
TomasHofman committed Apr 23, 2021
1 parent 3b461a2 commit 906907a
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 2 deletions.
4 changes: 4 additions & 0 deletions extensions/hibernate-orm/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-hibernate-common-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PersistenceUnitDescriptorBuildItem> 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();
}

}
Original file line number Diff line number Diff line change
@@ -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 !}

<a href="{urlbase}/persistence-units" class="badge badge-light">
<i class="fa fa-boxes fa-fw"></i>
Persistence units <span class="badge badge-light"># {info:persistenceUnits.size}</span></a>


{#for unit in info:persistenceUnits}
<a href="/q/pu-schema/{unit.persistenceUnitName}" class="badge badge-light">
{unit.persistenceUnitName}</a>
{/for}
Original file line number Diff line number Diff line change
@@ -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}
<p>No persistence units found.</p>
{#else}
<p>Count: {info:persistenceUnits}</p>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">Persistence Unit Name</th>
<th scope="col">Size</th>
</tr>
</thead>
<tbody>
{#for unit in info:persistenceUnits}
<tr>
<td>
{unit.persistenceUnitName}
</td>
<td>
<ul>
{#for cls in unit.managedClassNames}
<li>{cls}</li>
{/for}
</ul>
</td>
</tr>
{/for}
</tbody>
</table>
{/if}

{/body}
{/include}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected]
Expand Down Expand Up @@ -119,4 +121,8 @@ protected Session delegate() {
};
}

public Handler<RoutingContext> schemaGenerationHandler() {
return new PersistenceUnitSchemaGenerationHandler();
}

}
Original file line number Diff line number Diff line change
@@ -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<RoutingContext> {
@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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<PersistenceUnitDescriptor> convertPersistenceUnits(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<TargetType> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 906907a

Please sign in to comment.