
org.nuiton.topia.persistence.internal.AbstractTopiaApplicationContext Maven / Gradle / Ivy
package org.nuiton.topia.persistence.internal;
/*
* #%L
* ToPIA Extension :: API
* %%
* Copyright (C) 2018 - 2022 Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
* #L%
*/
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.hibernate.tool.schema.TargetType;
import org.nuiton.topia.persistence.TopiaApplicationContext;
import org.nuiton.topia.persistence.TopiaConfigurationBuilder;
import org.nuiton.topia.persistence.TopiaConfigurationExtension;
import org.nuiton.topia.persistence.TopiaDao;
import org.nuiton.topia.persistence.TopiaEntity;
import org.nuiton.topia.persistence.TopiaException;
import org.nuiton.topia.persistence.TopiaIdFactory;
import org.nuiton.topia.persistence.TopiaIdFactoryForBulkSupport;
import org.nuiton.topia.persistence.TopiaMigrationService;
import org.nuiton.topia.persistence.TopiaPersistenceContext;
import org.nuiton.topia.persistence.TopiaService;
import org.nuiton.topia.persistence.internal.support.TopiaServiceSupportImpl;
import org.nuiton.topia.persistence.jdbc.JdbcHelper;
import io.ultreia.java4all.util.sql.SqlScript;
import io.ultreia.java4all.util.sql.SqlScriptConsumer;
import io.ultreia.java4all.util.sql.SqlScriptReader;
import io.ultreia.java4all.util.sql.SqlScriptWriter;
import org.nuiton.topia.persistence.support.TopiaServiceSupport;
import org.nuiton.topia.persistence.util.TopiaUtil;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.function.Supplier;
/**
* The application context is the main class in ToPIA usage. This class is a kind of equivalent of the RootTopiaContext.
* It contains only high level methods and new contexts creation (transaction begin, ...). This class has to be extended
* by the user, even if some default one could be automatically generated.
*
* @author Arnaud Thimel (Code Lutin)
* @since 3.0
*/
public abstract class AbstractTopiaApplicationContext implements TopiaApplicationContext {
private static final Logger log = LogManager.getLogger(AbstractTopiaApplicationContext.class);
public static final String MIGRATION_SERVICE_NAME = "migration";
public static final String SQL_SERVICE_NAME = "sql";
protected TopiaIdFactory topiaIdFactory;
protected TopiaConfigurationExtension configuration;
protected TopiaServiceSupport topiaServiceSupport;
protected HibernateProvider hibernateProvider;
protected TopiaHibernateSessionRegistry sessionRegistry;
/**
* Dao mapping.
*/
private transient Map>> daoMapping;
/**
* Unique id associated with this application context.
*/
protected final String authenticationToken = UUID.randomUUID().toString();
/**
* Set to {@code true} when at least one transaction has been done.
*/
protected boolean open;
protected boolean closed = false;
/**
* A set of known initialized opened persistence contexts. This set is mainly used to trigger shutdown on these
* persistence contexts when the application context is closed.
*/
protected Set persistenceContexts = Collections.newSetFromMap(
new WeakHashMap<>());
public AbstractTopiaApplicationContext(TopiaConfigurationExtension configuration) {
this.configuration = configuration;
init();
}
public Map>> getDaoMapping() {
if (daoMapping == null) {
daoMapping = createDaoMapping();
}
return daoMapping;
}
protected abstract Map>> createDaoMapping();
public abstract TopiaIdFactoryForBulkSupport newIdFactoryForBulk(long timestamp);
@Override
public final String getAuthenticationToken() {
return authenticationToken;
}
public String getPackageName() {
return getClass().getPackageName();
}
protected void init() {
new TopiaConfigurationBuilder().check(configuration);
sessionRegistry = new TopiaHibernateSessionRegistry();
// First initialize all the services
initServices();
// ToPIA's schema init
if (isInitSchema()) {
if (log.isInfoEnabled()) {
log.info("Schema initialization enabled");
}
initSchema();
} else {
if (log.isInfoEnabled()) {
log.info("Schema initialization disabled");
}
}
// AThimel 14/06/14 Make sure this method is called AFTER ToPIA's schema init, otherwise Hibernate may have created the schema itself
// The next line will trigger the Configuration#buildMappings() method which really initializes Hibernate
getHibernateProvider().getHibernateConfiguration();
}
protected void initServices() {
TopiaServiceSupportImpl topiaServiceSupportImpl = new TopiaServiceSupportImpl();
this.topiaServiceSupport = topiaServiceSupportImpl;
topiaServiceSupportImpl.initServices(this);
}
/**
* If application context should init schema.
*
* This simple check is in its own method so it can be overridden.
*
* @return FIXME
* @see org.nuiton.topia.persistence.TopiaConfiguration#isInitSchema()
*/
protected boolean isInitSchema() {
return configuration.isInitSchema();
}
/**
* Will make everything possible to ensure the schema is ready to use.
*
* It will create unless it already exists. If it already exists, we will try to find
* a migration service and call it.
*/
@Override
public void initSchema() {
Collection migrationServices =
getServices(TopiaMigrationService.class).values();
Preconditions.checkState(migrationServices.size() <= 1,
"your configuration include multiple migration services: " + migrationServices);
boolean migrationServiceEnabled = !migrationServices.isEmpty();
if (isSchemaEmpty()) {
if (log.isInfoEnabled()) {
log.info("schema is empty, will create");
}
createSchema();
if (migrationServiceEnabled) {
if (log.isInfoEnabled()) {
log.info("schema created, will call migration service");
}
TopiaMigrationService migrationService =
Iterables.getOnlyElement(migrationServices);
migrationService.initOnCreateSchema();
} else {
if (log.isInfoEnabled()) {
log.info("schema created, no migration service provided");
}
}
} else {
if (migrationServiceEnabled) {
if (log.isInfoEnabled()) {
log.info("schema exists, will try to migrate");
}
TopiaMigrationService migrationService =
Iterables.getOnlyElement(migrationServices);
migrationService.runSchemaMigration();
} else {
if (log.isInfoEnabled()) {
log.info("schema exists, no migration service provided");
}
}
}
}
protected abstract Set> getImplementationClasses();
protected void registerPersistenceContext(TopiaPersistenceContext persistenceContext) {
persistenceContexts.add(persistenceContext);
}
// FIXME AThimel 25/11/13 I don't like it to be public, but necessary for services. Review it
public HibernateProvider getHibernateProvider() {
if (hibernateProvider == null) {
// La liste des entités (ToPIA ou non) qu'Hibernate gère
List> persistenceClasses = getPersistenceClasses();
hibernateProvider = new HibernateProvider(getConfiguration(), topiaServiceSupport, sessionRegistry, persistenceClasses);
}
return hibernateProvider;
}
@Override
public TopiaConfigurationExtension getConfiguration() {
return configuration;
}
protected TopiaIdFactory getTopiaIdFactory() {
return getConfiguration().getTopiaIdFactory();
}
public TopiaHibernateSessionRegistry getSessionRegistry() {
return sessionRegistry;
}
@Override
public Map getServices() {
return topiaServiceSupport.getServices();
}
@Override
public Map getServices(Class interfaceService) {
return topiaServiceSupport.getServices(interfaceService);
}
@Override
public List> getPersistenceClasses() {
// Par défaut la liste des entités qu'Hibernate gère est seulement la liste des entités ToPIA mais en
// surchargeant cette méthode on peut ajouter des entités non ToPIA
return new ArrayList<>(getImplementationClasses());
}
@Override
public boolean isSchemaEmpty() {
// AThimel 14/06/14 getHibernateConfiguration() may create the schema, prefer using newHibernateConfiguration() which doesn't
Configuration configuration = getHibernateProvider().newHibernateConfiguration();
configuration.getProperties().put(AvailableSettings.HBM2DDL_AUTO, "none");
try (SessionFactory sessionFactory = hibernateProvider.newSessionFactory(configuration)) {
Metadata metaData = getHibernateProvider().newMetaData(configuration, sessionFactory);
return TopiaUtil.isSchemaEmpty(configuration, metaData);
}
}
@Override
public boolean isTableExists(Class> clazz) {
// AThimel 14/06/14 getHibernateConfiguration() may create the schema, prefer using newHibernateConfiguration() which doesn't
Configuration configuration = getHibernateProvider().getHibernateConfiguration();
Metadata metaData = getHibernateProvider().getMetaData();
return TopiaUtil.isSchemaExist(configuration, metaData, clazz.getName());
}
@Override
public String getSchemaName() {
// TODO AThimel 02/08/13 I absolutely don't know if it works
return getConfiguration().getSchemaName();
}
@Override
public void createSchema() {
try {
EnumSet targetTypes = EnumSet.of(TargetType.DATABASE);
if (log.isDebugEnabled()) {
targetTypes.add(TargetType.STDOUT);
}
Configuration configuration = getHibernateProvider().newHibernateConfiguration();
configuration.getProperties().remove(AvailableSettings.HBM2DDL_AUTO);
try (SessionFactory sessionFactory = getHibernateProvider().newSessionFactory(configuration)) {
Metadata metadata = getHibernateProvider().newMetaData(configuration, sessionFactory);
new SchemaExport().execute(targetTypes, SchemaExport.Action.CREATE, metadata);
}
} catch (HibernateException eee) {
throw new TopiaException(String.format("Could not create schema for reason: %s", eee.getMessage()), eee);
}
}
@Override
public void showCreateSchema() {
try {
new SchemaExport().execute(EnumSet.of(TargetType.DATABASE, TargetType.STDOUT), SchemaExport.Action.CREATE, getHibernateProvider().getMetaData());
} catch (HibernateException eee) {
throw new TopiaException(String.format("Could not show create schema for reason: %s", eee.getMessage()), eee);
}
}
@Override
public void updateSchema() {
try {
EnumSet targetTypes = EnumSet.of(TargetType.DATABASE);
if (log.isDebugEnabled()) {
targetTypes.add(TargetType.STDOUT);
}
new SchemaUpdate().execute(targetTypes, getHibernateProvider().getMetaData());
} catch (HibernateException eee) {
throw new TopiaException(String.format("Could not update schema for reason: %s", eee.getMessage()), eee);
}
}
@Override
public void dropSchema() {
try {
EnumSet targetTypes = EnumSet.of(TargetType.DATABASE);
if (log.isDebugEnabled()) {
targetTypes.add(TargetType.STDOUT);
}
new SchemaExport().execute(targetTypes, SchemaExport.Action.DROP, getHibernateProvider().getMetaData());
} catch (HibernateException eee) {
throw new TopiaException(String.format("Could not drop schema for reason: %s", eee.getMessage()), eee);
}
}
@Override
public void close() {
// Throw exception if context is already closed
Preconditions.checkState(!closed, "TopiaApplicationContext was already closed");
if (log.isDebugEnabled()) {
log.debug("will close " + this);
}
TopiaConfigurationExtension topiaConfiguration = getConfiguration();
if (topiaConfiguration.isH2Configuration()) {
log.debug("Shutdown h2 database");
//FIXME Should we still do this ?
// Fermer proprement la base
JdbcHelper jdbcHelper = new JdbcHelper(topiaConfiguration);
jdbcHelper.runUpdate("SHUTDOWN COMPACT;");
}
// Iterate over the children PersistenceContexts and close them
for (TopiaPersistenceContext persistenceContext : persistenceContexts) {
if (persistenceContext == null) {
if (log.isWarnEnabled()) {
log.warn("null TopiaPersistenceContext found in #persistenceContexts");
}
} else {
// Avoid to have exception from checkNotClosed method on child
try {
if (!persistenceContext.isClosed()) {
persistenceContext.close();
}
} catch (Exception eee) {
// Don't let any exception stop the application closing
if (log.isWarnEnabled()) {
log.warn("unable to close TopiaPersistenceContext", eee);
}
}
}
}
hibernateProvider.close();
// call TopiaService#close on all services
for (TopiaService topiaService : getServices().values()) {
topiaService.close();
}
closed = true;
log.debug(this + " closed");
}
@Override
public ImmutableSet getSchemaNames() {
ImmutableSet.Builder result = ImmutableSet.builder();
Collection classMappings = hibernateProvider.getMetaData().getEntityBindings();
for (PersistentClass persistentClass : classMappings) {
String schema = persistentClass.getIdentityTable().getSchema();
if (StringUtils.isNotEmpty(schema)) {
result.add(schema);
}
}
return result.build();
}
@Override
public boolean isClosed() {
return closed;
}
@Override
public boolean isOpened() {
return !isClosed();
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AbstractTopiaApplicationContext)) return false;
AbstractTopiaApplicationContext> that = (AbstractTopiaApplicationContext>) o;
return Objects.equals(authenticationToken, that.authenticationToken);
}
@Override
public final int hashCode() {
return Objects.hash(authenticationToken);
}
public final boolean isOpen() {
return isOpened() && open;
}
public final void setOpen(boolean open) {
this.open = open;
}
public TopiaMigrationService getMigrationService() {
return getServices(TopiaMigrationService.class).get(MIGRATION_SERVICE_NAME);
}
public final void migrate() {
getMigrationService().runSchemaMigration();
}
public final void backup(File backupFile) {
backup(backupFile, true);
}
public final void backup(File backupFile, boolean compress) {
if (!getConfiguration().isH2Configuration()) {
throw new IllegalStateException("Cant backup a none H2 database.");
}
try (K p = newPersistenceContext()) {
String sqlScript = String.format("SCRIPT NOPASSWORDS NOSETTINGS BLOCKSIZE 2048 TO '%s' %s CHARSET 'UTF-8';", backupFile.getAbsolutePath(), compress ? "COMPRESSION GZIP" : "");
p.getSqlSupport().executeSql(sqlScript);
}
}
public final void executeSqlStatements(SqlScript content) {
try {
executeSqlStatements0(content);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new TopiaException(e);
}
}
public final void executeSqlStatements(SqlScriptConsumer content) {
try {
executeSqlStatements0(content);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new TopiaException(e);
}
}
protected final void executeSqlStatements0(SqlScript content) {
SqlScriptReader scriptLocation = content.getLocation();
try (K persistenceContext = newPersistenceContext()) {
persistenceContext.executeSqlScript(scriptLocation);
persistenceContext.commit();
}
}
protected final void executeSqlStatements0(SqlScriptConsumer content) {
try (K persistenceContext = newPersistenceContext()) {
persistenceContext.executeSqlScript(content);
persistenceContext.commit();
}
}
protected SqlScriptWriter newWriter(Path backupFile, boolean compress) {
if (compress) {
return SqlScriptWriter.builder(backupFile).keepCommentLine().keepEmptyLine().gzip().build();
}
return SqlScriptWriter.builder(backupFile).keepCommentLine().keepEmptyLine().build();
}
}