com.manydesigns.portofino.persistence.Persistence Maven / Gradle / Ivy
/*
* Copyright (C) 2005-2020 ManyDesigns srl. All rights reserved.
* http://www.manydesigns.com/
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.manydesigns.portofino.persistence;
import com.manydesigns.portofino.PortofinoProperties;
import com.manydesigns.portofino.cache.CacheResetEvent;
import com.manydesigns.portofino.cache.CacheResetListenerRegistry;
import com.manydesigns.portofino.database.multitenancy.MultiTenant;
import com.manydesigns.portofino.liquibase.VFSResourceAccessor;
import com.manydesigns.portofino.model.Model;
import com.manydesigns.portofino.model.database.*;
import com.manydesigns.portofino.model.database.platforms.DatabasePlatformsRegistry;
import com.manydesigns.portofino.modules.DatabaseModule;
import com.manydesigns.portofino.persistence.hibernate.HibernateDatabaseSetup;
import com.manydesigns.portofino.persistence.hibernate.SessionFactoryAndCodeBase;
import com.manydesigns.portofino.persistence.hibernate.SessionFactoryBuilder;
import com.manydesigns.portofino.persistence.hibernate.multitenancy.MultiTenancyImplementation;
import com.manydesigns.portofino.persistence.hibernate.multitenancy.MultiTenancyImplementationFactory;
import com.manydesigns.portofino.reflection.TableAccessor;
import com.manydesigns.portofino.reflection.ViewAccessor;
import com.manydesigns.portofino.sync.DatabaseSyncer;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.PublishSubject;
import liquibase.Contexts;
import liquibase.Liquibase;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.resource.ResourceAccessor;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileType;
import org.hibernate.MultiTenancyStrategy;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Connection;
import java.util.*;
/**
* @author Paolo Predonzani - [email protected]
* @author Angelo Lupo - [email protected]
* @author Giampiero Granatella - [email protected]
* @author Alessio Stalla - [email protected]
*/
public class Persistence {
public static final String copyright =
"Copyright (C) 2005-2020 ManyDesigns srl";
//**************************************************************************
// Constants
//**************************************************************************
public static final String APP_MODEL_DIRECTORY = "portofino-model";
@Deprecated
public static final String APP_MODEL_FILE = APP_MODEL_DIRECTORY + ".xml";
public static final String LIQUIBASE_CONTEXT = "liquibase.context";
public final static String changelogFileNameTemplate = "liquibase.changelog.xml";
//**************************************************************************
// Fields
//**************************************************************************
protected final DatabasePlatformsRegistry databasePlatformsRegistry;
protected Model model;
protected final Map setups;
protected final FileObject applicationDirectory;
protected final Configuration configuration;
protected final FileBasedConfigurationBuilder configurationFile;
@Autowired
protected MultiTenancyImplementationFactory multiTenancyImplementationFactory = MultiTenancyImplementationFactory.DEFAULT;
public final BehaviorSubject status = BehaviorSubject.create();
public final PublishSubject databaseSetupEvents = PublishSubject.create();
public enum Status {
STARTING, STARTED, STOPPING, STOPPED
}
public CacheResetListenerRegistry cacheResetListenerRegistry;
//**************************************************************************
// Logging
//**************************************************************************
public static final Logger logger =
LoggerFactory.getLogger(Persistence.class);
//**************************************************************************
// Constructors
//**************************************************************************
public Persistence(
FileObject applicationDirectory, Configuration configuration,
FileBasedConfigurationBuilder configurationFile,
DatabasePlatformsRegistry databasePlatformsRegistry) throws FileSystemException {
this.applicationDirectory = applicationDirectory;
this.configuration = configuration;
this.configurationFile = configurationFile;
this.databasePlatformsRegistry = databasePlatformsRegistry;
if(getModelFile().exists()) {
logger.info("Legacy application model file: {}", getModelFile().getName().getPath());
} else {
logger.info("Application model directory: {}", getModelDirectory().getName().getPath());
}
setups = new HashMap<>();
}
//**************************************************************************
// Model loading
//**************************************************************************
public synchronized void loadXmlModel() {
try {
JAXBContext jc = createModelJAXBContext();
Unmarshaller um = jc.createUnmarshaller();
FileObject appModelFile = getModelFile();
if(appModelFile.exists()) {
logger.info("Loading legacy xml model from file: {}", appModelFile.getName().getPath());
try (InputStream inputStream = appModelFile.getContent().getInputStream()) {
model = (Model) um.unmarshal(inputStream);
} catch (Exception e) {
String msg = "Cannot load/parse model: " + appModelFile;
logger.error(msg, e);
}
} else {
logger.info("Loading model from directory: {}", getModelDirectory().getName().getPath());
model = new Model();
}
FileObject modelDir = getModelDirectory();
if(modelDir.exists()) {
for (FileObject databaseDir : modelDir.getChildren()) {
loadXmlDatabase(um, model, databaseDir);
}
}
initModel();
} catch (Exception e) {
logger.error("Cannot load/parse model", e);
}
}
public JAXBContext createModelJAXBContext() throws JAXBException {
return JAXBContext.newInstance(Model.class, View.class);
}
protected void loadXmlDatabase(Unmarshaller um, Model model, FileObject databaseDir) throws IOException, JAXBException {
if(!databaseDir.getType().equals(FileType.FOLDER)) {
logger.error("Not a directory: " + databaseDir.getName().getPath());
return;
}
String databaseName = databaseDir.getName().getBaseName();
FileObject databaseFile = databaseDir.resolveFile("database.xml");
Database database;
if(databaseFile.exists()) {
logger.info("Loading database connection from " + databaseFile.getName().getPath());
try(InputStream inputStream = databaseFile.getContent().getInputStream()) {
database = (Database) um.unmarshal(inputStream);
database.afterUnmarshal(um, model);
if(!databaseName.equals(database.getDatabaseName())) {
logger.error("Database named {} defined in directory named {}, skipping", database.getDatabaseName(), databaseName);
return;
}
model.getDatabases().removeIf(d -> databaseName.equals(d.getDatabaseName()));
model.getDatabases().add(database);
}
FileObject settingsFile = databaseDir.resolveFile("hibernate.properties");
if(settingsFile.exists()) {
try(InputStream inputStream = settingsFile.getContent().getInputStream()) {
Properties settings = new Properties();
settings.load(inputStream);
database.setSettings(settings);
}
}
} else {
database = DatabaseLogic.findDatabaseByName(model, databaseName);
if(database != null) {
logger.info("Using legacy database defined in portofino-model.xml: " + databaseName + "; it will be automatically migrated to database.xml upon save.");
} else {
logger.warn("No database defined in " + databaseDir.getName().getPath());
return;
}
}
for(Schema schema : database.getSchemas()) {
FileObject schemaDir = databaseDir.resolveFile(schema.getSchemaName());
if(schemaDir.getType() == FileType.FOLDER) {
logger.debug("Schema directory {} exists", schemaDir);
FileObject[] tableFiles = schemaDir.getChildren();
for(FileObject tableFile : tableFiles) {
if(!tableFile.getName().getBaseName().endsWith(".table.xml")) {
continue;
}
try(InputStream tableInputStream = tableFile.getContent().getInputStream()) {
Table table = (Table) um.unmarshal(tableInputStream);
if (!tableFile.getName().getBaseName().equalsIgnoreCase(table.getTableName() + ".table.xml")) {
logger.error("Skipping table " + table.getTableName() + " defined in file " + tableFile);
continue;
}
table.afterUnmarshal(um, schema);
schema.getTables().add(table);
}
}
} else {
logger.debug("Schema directory {} does not exist", schemaDir);
}
}
}
@Deprecated
public FileObject getModelFile() throws FileSystemException {
return applicationDirectory.resolveFile(APP_MODEL_FILE);
}
public FileObject getModelDirectory() throws FileSystemException {
return applicationDirectory.resolveFile(APP_MODEL_DIRECTORY);
}
public void runLiquibase(Database database) {
logger.info("Updating database definitions");
ResourceAccessor resourceAccessor = new VFSResourceAccessor(applicationDirectory);
ConnectionProvider connectionProvider = database.getConnectionProvider();
for(Schema schema : database.getSchemas()) {
String schemaName = schema.getSchemaName();
try(Connection connection = connectionProvider.acquireConnection()) {
FileObject changelogFile = getLiquibaseChangelogFile(schema);
if(changelogFile.getType() != FileType.FILE) {
logger.info("Changelog file does not exist or is not a normal file, skipping: {}", changelogFile);
continue;
}
logger.info("Running changelog file: {}", changelogFile);
JdbcConnection jdbcConnection = new JdbcConnection(connection);
liquibase.database.Database lqDatabase =
DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection);
lqDatabase.setDefaultSchemaName(schema.getActualSchemaName());
String relativeChangelogPath = applicationDirectory.getName().getRelativeName(changelogFile.getName());
Liquibase lq = new Liquibase(relativeChangelogPath, resourceAccessor, lqDatabase);
String[] contexts = configuration.getStringArray(LIQUIBASE_CONTEXT);
logger.info("Using context {}", Arrays.toString(contexts));
lq.update(new Contexts(contexts));
} catch (Exception e) {
logger.error("Couldn't update database: " + schemaName, e);
}
}
}
public synchronized void saveXmlModel() throws IOException, JAXBException, ConfigurationException {
//TODO gestire conflitti con modifiche esterne?
JAXBContext jc = createModelJAXBContext();
Marshaller m = jc.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
FileObject modelDir = getModelDirectory();
modelDir.createFolder();
for(Database database : model.getDatabases()) {
FileObject databaseDir = modelDir.resolveFile(database.getDatabaseName());
FileObject databaseFile = databaseDir.resolveFile("database.xml");
databaseFile.createFile();
try(OutputStream outputStream = databaseFile.getContent().getOutputStream()) {
m.marshal(database, outputStream);
}
for(Schema schema : database.getSchemas()) {
FileObject schemaDir = databaseDir.resolveFile(schema.getSchemaName());
if(!schemaDir.exists()) {
logger.debug("Schema directory {} does not exist", schemaDir);
schemaDir.createFolder();
}
FileObject[] tableFiles = schemaDir.getChildren();
for(FileObject tableFile : tableFiles) {
if(tableFile.getName().getBaseName().endsWith(".table.xml")) {
if (!tableFile.delete()) {
logger.warn("Could not delete table file {}", tableFile.getName().getPath());
}
}
}
for(Table table : schema.getTables()) {
FileObject tableFile = schemaDir.resolveFile(table.getTableName() + ".table.xml");
try(OutputStream outputStream = tableFile.getContent().getOutputStream()) {
m.marshal(table, outputStream);
}
}
}
deleteUnusedSchemaDirectories(database, databaseDir);
}
deleteUnusedDatabaseDirectories();
logger.info("Saved xml model to directory: {}", modelDir.getName().getPath());
if(configurationFile != null) {
configurationFile.save();
logger.info("Saved configuration file {}", configurationFile.getFileHandler().getFile().getAbsolutePath());
}
FileObject appModelFile = getModelFile();
if(appModelFile.exists()) {
appModelFile.delete();
logger.info("Deleted legacy portofino-model.xml file: {}", appModelFile.getName().getPath());
}
}
/**
* Delete the directories of the databases that are no longer present in the model
*
* @throws FileSystemException if the schema directories cannot be listed.
*/
protected void deleteUnusedDatabaseDirectories() throws FileSystemException {
Arrays.stream(getModelDirectory().getChildren()).forEach(dbDir -> {
String dbDirPath = dbDir.getName().getPath();
try {
if(dbDir.getType() == FileType.FOLDER) {
String dirName = dbDir.getName().getBaseName();
if (model.getDatabases().stream().noneMatch(db -> db.getDatabaseName().equals(dirName))) {
logger.info("Deleting unused database directory {}", dbDirPath);
try {
dbDir.deleteAll();
} catch (FileSystemException e) {
logger.warn("Could not delete unused database dir " + dbDirPath, e);
}
}
}
} catch (FileSystemException e) {
logger.error("Unexpected filesystem error when trying to delete schema directory " + dbDirPath, e);
}
});
}
/**
* Delete the directories of the schemas that are no longer present in the model
*
* @param database the parent database containing the schemas
* @param databaseDir the database directory
* @throws FileSystemException if the schema directories cannot be listed.
*/
protected void deleteUnusedSchemaDirectories(Database database, FileObject databaseDir) throws FileSystemException {
Arrays.stream(databaseDir.getChildren()).forEach(schemaDir -> {
String schemaDirPath = schemaDir.getName().getPath();
try {
if(schemaDir.getType() == FileType.FOLDER) {
String dirName = schemaDir.getName().getBaseName();
if (database.getSchemas().stream().noneMatch(schema -> schema.getSchemaName().equals(dirName))) {
logger.info("Deleting unused schema directory {}", schemaDirPath);
try {
schemaDir.deleteAll();
} catch (FileSystemException e) {
logger.warn("Could not delete unused schema dir " + schemaDirPath, e);
}
}
}
} catch (FileSystemException e) {
logger.error("Unexpected filesystem error when trying to delete schema directory " + schemaDirPath, e);
}
});
}
public synchronized void initModel() {
logger.info("Cleaning up old setups");
closeSessions();
for (Map.Entry current : setups.entrySet()) {
String databaseName = current.getKey();
logger.debug("Cleaning up old setup for: {}", databaseName);
HibernateDatabaseSetup setup = current.getValue();
try {
setup.dispose();
} catch (Throwable t) {
logger.warn("Cannot close session factory for: " + databaseName, t);
}
databaseSetupEvents.onNext(new DatabaseSetupEvent(DatabaseSetupEvent.REMOVED, setup));
}
//TODO it would perhaps be preferable if we generated REPLACED events here rather than REMOVED followed by ADDED
setups.clear();
model.init(configuration);
for (Database database : model.getDatabases()) {
initConnectionProvider(database);
}
if(cacheResetListenerRegistry != null) {
cacheResetListenerRegistry.fireReset(new CacheResetEvent(this));
}
}
protected void initConnectionProvider(Database database) {
logger.info("Initializing connection provider for database " + database.getDatabaseName());
try {
ConnectionProvider connectionProvider = database.getConnectionProvider();
connectionProvider.init(databasePlatformsRegistry);
if (connectionProvider.getStatus().equals(ConnectionProvider.STATUS_CONNECTED)) {
MultiTenancyImplementation implementation = getMultiTenancyImplementation(database);
SessionFactoryBuilder builder = new SessionFactoryBuilder(database, configuration, implementation);
SessionFactoryAndCodeBase sessionFactoryAndCodeBase = builder.buildSessionFactory();
HibernateDatabaseSetup setup =
new HibernateDatabaseSetup(
database, sessionFactoryAndCodeBase.sessionFactory,
sessionFactoryAndCodeBase.codeBase, builder.getEntityMode(), configuration,
implementation);
String databaseName = database.getDatabaseName();
HibernateDatabaseSetup oldSetup = setups.get(databaseName);
setups.put(databaseName, setup);
if(oldSetup != null) {
oldSetup.dispose();
databaseSetupEvents.onNext(new DatabaseSetupEvent(oldSetup, setup));
} else {
databaseSetupEvents.onNext(new DatabaseSetupEvent(DatabaseSetupEvent.ADDED, setup));
}
}
} catch (Exception e) {
logger.error("Could not create connection provider for " + database, e);
}
}
protected MultiTenancyImplementation getMultiTenancyImplementation(Database database) {
Optional multiTenant = database.getJavaAnnotation(MultiTenant.class);
if(multiTenant.isPresent()) {
Class extends MultiTenancyImplementation> implClass = multiTenant.get().strategy();
//TODO injection?
if(!MultiTenancyImplementation.class.isAssignableFrom(implClass)) {
throw new ClassCastException(implClass + " does not extend " + MultiTenancyImplementation.class);
}
try {
MultiTenancyImplementation implementation = multiTenancyImplementationFactory.make(implClass);
MultiTenancyStrategy strategy = implementation.getStrategy();
if (strategy.requiresMultiTenantConnectionProvider()) {
return implementation;
}
} catch (Exception e) {
throw new RuntimeException("Could not instantiate multi tenancy implementation " + implClass + " for " + database, e);
}
}
return null;
}
public void retryFailedConnections() {
Status currentStatus = status.getValue();
if(currentStatus != Status.STARTED) {
throw new IllegalStateException("Persistence not started: " + currentStatus);
}
for (Database database : model.getDatabases()) {
if (!ConnectionProvider.STATUS_CONNECTED.equals(database.getConnectionProvider().getStatus())) {
logger.info("Retrying failed connection to database " + database.getDatabaseName());
initConnectionProvider(database);
}
}
}
//**************************************************************************
// Database stuff
//**************************************************************************
public ConnectionProvider getConnectionProvider(String databaseName) {
for (Database database : model.getDatabases()) {
if (database.getDatabaseName().equals(databaseName)) {
return database.getConnectionProvider();
}
}
return null;
}
public Configuration getConfiguration() {
return configuration;
}
public DatabasePlatformsRegistry getDatabasePlatformsRegistry() {
return databasePlatformsRegistry;
}
//**************************************************************************
// Model access
//**************************************************************************
public Model getModel() {
return model;
}
public synchronized void syncDataModel(String databaseName) throws Exception {
Database sourceDatabase = DatabaseLogic.findDatabaseByName(model, databaseName);
if(sourceDatabase == null) {
throw new IllegalArgumentException("Database " + databaseName + " does not exist");
}
if(configuration.getBoolean(DatabaseModule.LIQUIBASE_ENABLED, true)) {
runLiquibase(sourceDatabase);
} else {
logger.debug("syncDataModel called, but Liquibase is not enabled");
}
ConnectionProvider connectionProvider = sourceDatabase.getConnectionProvider();
DatabaseSyncer dbSyncer = new DatabaseSyncer(connectionProvider);
Database targetDatabase = dbSyncer.syncDatabase(model);
model.getDatabases().remove(sourceDatabase);
model.getDatabases().add(targetDatabase);
}
//**************************************************************************
// Persistance
//**************************************************************************
public Session getSession(String databaseName) {
return ensureDatabaseSetup(databaseName).getThreadSession();
}
protected HibernateDatabaseSetup ensureDatabaseSetup(String databaseName) {
HibernateDatabaseSetup setup = getDatabaseSetup(databaseName);
if (setup == null) {
throw new Error("No setup exists for database: " + databaseName);
}
return setup;
}
public HibernateDatabaseSetup getDatabaseSetup(String databaseName) {
return setups.get(databaseName);
}
public void closeSessions() {
for (HibernateDatabaseSetup current : setups.values()) {
closeSession(current);
}
}
public void closeSession(String databaseName) {
closeSession(ensureDatabaseSetup(databaseName));
}
protected void closeSession(HibernateDatabaseSetup current) {
Session session = current.getThreadSession(false);
if (session != null) {
try {
Transaction transaction = session.getTransaction();
if(transaction != null && transaction.isActive()) {
transaction.rollback();
}
session.close();
} catch (Throwable e) {
logger.warn("Couldn't close session: " + ExceptionUtils.getRootCauseMessage(e), e);
}
current.removeThreadSession();
}
}
public @NotNull TableAccessor getTableAccessor(String databaseName, String entityName) {
Database database = DatabaseLogic.findDatabaseByName(model, databaseName);
if(database == null) {
throw new IllegalArgumentException("Database " + databaseName + " does not exist");
}
Table table = DatabaseLogic.findTableByEntityName(database, entityName);
if(table == null) {
throw new IllegalArgumentException("Table " + entityName + " not found in database " + databaseName);
}
return getTableAccessor(table);
}
@NotNull
public TableAccessor getTableAccessor(Table table) {
return table instanceof View ? new ViewAccessor((View) table) : new TableAccessor(table);
}
//**************************************************************************
// User
//**************************************************************************
public void start() {
status.onNext(Status.STARTING);
loadXmlModel();
for(Database database : model.getDatabases()) {
if(ConnectionProvider.STATUS_CONNECTED.equals(database.getConnectionProvider().getStatus())) {
runLiquibase(database);
}
}
status.onNext(Status.STARTED);
}
public void stop() {
//TODO complete subscriptions?
status.onNext(Status.STOPPING);
closeSessions();
for(HibernateDatabaseSetup setup : setups.values()) {
setup.dispose();
}
for (Database database : model.getDatabases()) {
ConnectionProvider connectionProvider =
database.getConnectionProvider();
connectionProvider.shutdown();
}
status.onNext(Status.STOPPED);
databaseSetupEvents.onComplete();
status.onComplete();
}
//**************************************************************************
// App directories and files
//**************************************************************************
public String getName() {
return getConfiguration().getString(PortofinoProperties.APP_NAME);
}
public FileObject getLiquibaseChangelogFile(Schema schema) throws FileSystemException {
if(schema == null) {
return null;
}
FileObject dbDir = getModelDirectory().resolveFile(schema.getDatabaseName());
FileObject schemaDir = dbDir.resolveFile(schema.getSchemaName());
return schemaDir.resolveFile(changelogFileNameTemplate);
}
@Deprecated
public FileObject getAppModelFile() {
try {
return getModelFile();
} catch (FileSystemException e) {
throw new RuntimeException(e);
}
}
public FileObject getApplicationDirectory() {
return applicationDirectory;
}
public static class DatabaseSetupEvent {
public static final int ADDED = +1, REMOVED = -1, REPLACED = 0;
public DatabaseSetupEvent(int type, HibernateDatabaseSetup setup) {
if(type == 0) {
throw new IllegalArgumentException();
}
this.type = type;
this.setup = setup;
}
public DatabaseSetupEvent(HibernateDatabaseSetup setup, HibernateDatabaseSetup oldSetup) {
this.setup = setup;
this.oldSetup = oldSetup;
type = REPLACED;
}
public int type;
public HibernateDatabaseSetup setup;
public HibernateDatabaseSetup oldSetup;
}
public MultiTenancyImplementationFactory getMultiTenancyImplementationFactory() {
return multiTenancyImplementationFactory;
}
public void setMultiTenancyImplementationFactory(MultiTenancyImplementationFactory multiTenancyImplementationFactory) {
this.multiTenancyImplementationFactory = multiTenancyImplementationFactory;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy