/*
* The MIT License (MIT)
*
* Copyright (c) 2013 Brien L. Wheeler (brienwheeler@yahoo.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.brienwheeler.apps.schematool;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.MappingException;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.Settings;
import org.hibernate.cfg.SettingsFactory;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.connection.ConnectionProviderFactory;
import org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import com.brienwheeler.lib.spring.beans.PropertyPlaceholderConfigurer;
import com.brienwheeler.lib.spring.beans.SmartClassPathXmlApplicationContext;
/**
* A useful utility for dumping and/or executing database schema initialization or updates.
*
* <p>By default, this tool reads the Spring beans file
* <tt>classpath:com/brienwheeler/lib/db/appEntityManagerFactory.xml</tt>
* looking for a bean named <tt>com.brienwheeler.lib.db.appEntityManagerFactory</tt>.
* These values can be changed using the <tt>emfContextLocation</tt> and <tt>emfContextBeanName</tt>
* properties.
*
* <p>By default this tool only prints a set of SQL it believes is needed to update the
* schema and performs no modifications agaisnt the database. You may use the <tt>mode</tt>
* and <tt>exec</tt> properties to change this behavior. Setting Mode.CLEAN will instead generate
* SQL that would initialize an empty database.
*
* Setting exec true will cause the SQL to be executed against the database referred to by the
* emfContextBean. When mode == CLEAN and exec == true, database tables are dropped before being
* re-instantiated.
*
* @author Brien Wheeler
*/
public class SchemaToolBean implements InitializingBean, ApplicationListener<ContextRefreshedEvent>
{
private static final Log log = LogFactory.getLog(SchemaToolBean.class);
public static enum Mode
{
UPDATE,
CLEAN
}
private Mode mode = Mode.UPDATE;
private boolean exec = false;
private boolean closeContextOnDone = true;
private String emfContextLocation = null;
private String emfContextBeanName = null;
private String emfPersistenceLocationsPropName = null;
private String emfPersistenceLocationsPropValue = null;
@Required
public void setMode(Mode mode) {
this.mode = mode;
}
@Required
public void setExec(boolean exec) {
this.exec = exec;
}
@Required
public void setCloseContextOnDone(boolean closeContextOnDone) {
this.closeContextOnDone = closeContextOnDone;
}
@Required
public void setEmfContextLocation(String emfContextLocation) {
this.emfContextLocation = emfContextLocation;
}
@Required
public void setEmfContextBeanName(String emfContextBeanName) {
this.emfContextBeanName = emfContextBeanName;
}
@Required
public void setEmfPersistenceLocationsPropName(String emfPersistenceLocationsPropName)
{
this.emfPersistenceLocationsPropName = emfPersistenceLocationsPropName;
}
@Required
public void setEmfPersistenceLocationsPropValue(String emfPersistenceLocationsPropValue)
{
this.emfPersistenceLocationsPropValue = emfPersistenceLocationsPropValue;
}
public void afterPropertiesSet() throws Exception
{
try {
// set based on default values in schematool properties file if not already set
PropertyPlaceholderConfigurer.setProperty(emfPersistenceLocationsPropName, emfPersistenceLocationsPropValue);
Configuration configuration = determineHibernateConfiguration();
Settings settings = null;
if (exec || mode == Mode.UPDATE)
{
ClassPathXmlApplicationContext newContext = new SmartClassPathXmlApplicationContext(emfContextLocation);
try {
// get a reference to the factory bean, don't have it create a new EntityManager
LocalContainerEntityManagerFactoryBean factoryBean = newContext.getBean("&" + emfContextBeanName, LocalContainerEntityManagerFactoryBean.class);
SettingsFactory settingsFactory = new InjectedDataSourceSettingsFactory(factoryBean.getDataSource());
settings = settingsFactory.buildSettings(new Properties());
}
finally {
newContext.close();
}
}
if (mode == Mode.UPDATE)
{
SchemaUpdate update = new SchemaUpdate(configuration, settings);
update.execute(true, exec);
}
else
{
SchemaExport export = exec ? new SchemaExport(configuration, settings) :
new SchemaExport(configuration);
export.create(true, exec);
}
}
catch (Exception e) {
log.error("Error running SchemaTool", e);
throw e;
}
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event)
{
if (closeContextOnDone)
((AbstractApplicationContext) event.getApplicationContext()).close();
}
private Configuration determineHibernateConfiguration() throws MappingException, ClassNotFoundException, IOException
{
// Read in the bean definitions but don't go creating all the singletons,
// because that causes creation of data sources and connections to database, etc.
NonInitializingClassPathXmlApplicationContext context = new NonInitializingClassPathXmlApplicationContext(
new String[] { emfContextLocation });
try {
context.loadBeanDefinitions();
// Get well-known EntityManagerFactory bean definition by name
BeanDefinition emfBeanDef = context.getBeanDefinition(emfContextBeanName);
if (emfBeanDef == null)
{
throw new RuntimeException("no bean defined: " + emfContextBeanName);
}
// Get the name of the persistence unit for this EntityManagerFactory
PropertyValue puNameProperty = emfBeanDef.getPropertyValues().getPropertyValue("persistenceUnitName");
if (puNameProperty == null || !(puNameProperty.getValue() instanceof TypedStringValue))
{
throw new RuntimeException("no property 'persistenceUnitName' defined on bean: " + emfContextBeanName);
}
String puName = ((TypedStringValue) puNameProperty.getValue()).getValue();
// Get the name of the persistence unit for this EntityManagerFactory
PropertyValue pumProperty = emfBeanDef.getPropertyValues().getPropertyValue("persistenceUnitManager");
PersistenceUnitManager pum = null;
if (pumProperty != null)
{
pum = createConfiguredPum(context, pumProperty);
}
else
{
pum = simulateDefaultPum(context, emfBeanDef);
}
// create the Hibernate configuration
PersistenceUnitInfo pui = pum.obtainPersistenceUnitInfo(puName);
Configuration configuration = new Configuration();
configuration.setProperties(pui.getProperties());
for (String className : pui.getManagedClassNames())
{
configuration.addAnnotatedClass(Class.forName(className));
}
return configuration;
}
finally {
context.close();
}
}
private PersistenceUnitManager createConfiguredPum(NonInitializingClassPathXmlApplicationContext context,
PropertyValue pumProperty)
{
if (pumProperty.getValue() instanceof BeanDefinitionHolder)
{
String beanName = ((BeanDefinitionHolder) pumProperty.getValue()).getBeanName();
BeanDefinition beanDefinition = ((BeanDefinitionHolder) pumProperty.getValue()).getBeanDefinition();
return (PersistenceUnitManager) context.createBean(beanName, beanDefinition);
}
else
{
throw new RuntimeException("property 'persistenceUnitManager' is not a BeanDefinition on bean: " + emfContextBeanName);
}
}
@SuppressWarnings("unchecked")
private PersistenceUnitManager simulateDefaultPum(NonInitializingClassPathXmlApplicationContext context,
BeanDefinition emfBeanDef)
{
// Simulate Spring's use of DefaultPersistenceUnitManager
DefaultPersistenceUnitManager defpum = new DefaultPersistenceUnitManager();
// Set the location of the persistence XML -- when using the DPUM,
// you can only set one persistence XML location on the EntityManagerFactory
PropertyValue locationProperty = emfBeanDef.getPropertyValues().getPropertyValue("persistenceXmlLocation");
if (locationProperty == null || !(locationProperty.getValue() instanceof TypedStringValue))
{
throw new RuntimeException("no property 'persistenceXmlLocation' defined on bean: " + emfContextBeanName);
}
// Since PersistenceUnitPostProcessors may do things like set properties
// onto the persistence unit, we need to instantiate them here so that
// they get called when preparePersistenceUnitInfos() executes
PropertyValue puiPostProcProperty = emfBeanDef.getPropertyValues().getPropertyValue("persistenceUnitPostProcessors");
if (puiPostProcProperty != null && puiPostProcProperty.getValue() instanceof ManagedList)
{
List<PersistenceUnitPostProcessor> postProcessors = new ArrayList<PersistenceUnitPostProcessor>();
for (BeanDefinitionHolder postProcBeanDef : (ManagedList<BeanDefinitionHolder>) puiPostProcProperty.getValue())
{
String beanName = postProcBeanDef.getBeanName();
BeanDefinition beanDefinition = postProcBeanDef.getBeanDefinition();
PersistenceUnitPostProcessor postProcessor = (PersistenceUnitPostProcessor) context.createBean(beanName, beanDefinition);
postProcessors.add(postProcessor);
}
defpum.setPersistenceUnitPostProcessors(postProcessors.toArray(new PersistenceUnitPostProcessor[postProcessors.size()]));
}
defpum.setPersistenceXmlLocation(((TypedStringValue) locationProperty.getValue()).getValue());
defpum.preparePersistenceUnitInfos();
return defpum;
}
/**
* A context that provides a loadBeanDefinitions method that performs only
* the portions of the traditional refresh() required to define all the beans
* and substitute any placeholders.
*/
private static class NonInitializingClassPathXmlApplicationContext extends
ClassPathXmlApplicationContext
{
private ConfigurableListableBeanFactory beanFactory;
public NonInitializingClassPathXmlApplicationContext(
String[] configLocations) throws BeansException
{
super(configLocations, false, null);
}
public void loadBeanDefinitions() throws IOException
{
/*
* This method mirrors the refresh() method up to and including
* invoking the bean factory post processors, so that a complete set
* of bean definitions with resolved placeholders exists.
*/
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
//registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
//initMessageSource();
// Initialize event multicaster for this context, otherwise close() throws IllegalStateException
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
//onRefresh();
// Check for listener beans and register them.
//registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
//finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event, otherwise close() throws IllegalStateException
finishRefresh();
}
public BeanDefinition getBeanDefinition(String beanName)
{
return beanFactory.getBeanDefinition(beanName);
}
@Override
protected DefaultListableBeanFactory createBeanFactory()
{
return new BeanDefinitionCreatingFactory(getInternalParentBeanFactory());
}
public Object createBean(String beanName, BeanDefinition beanDefinition)
{
return ((BeanDefinitionCreatingFactory) beanFactory).createBean(beanName, beanDefinition);
}
}
/**
* A BeanFactory that exposes a method that creates from the BeanDefinition
*/
private static class BeanDefinitionCreatingFactory extends DefaultListableBeanFactory
{
public BeanDefinitionCreatingFactory(BeanFactory parentBeanFactory)
{
super(parentBeanFactory);
}
public Object createBean(String beanName, BeanDefinition beanDefinition)
{
RootBeanDefinition rootBeanDefinition = getMergedBeanDefinition(beanName, beanDefinition);
return createBean(beanName, rootBeanDefinition, null);
}
}
/**
* A helper class to create a Hibernate Settings object that uses an
* InjectedDataSourceConnectionProvider.
*/
private static class InjectedDataSourceSettingsFactory extends SettingsFactory
{
private static final long serialVersionUID = 1L;
private DataSource dataSource;
public InjectedDataSourceSettingsFactory(DataSource dataSource)
{
super();
this.dataSource = dataSource;
}
@Override
protected ConnectionProvider createConnectionProvider(Properties properties)
{
properties.setProperty(Environment.CONNECTION_PROVIDER,
InjectedDataSourceConnectionProvider.class.getCanonicalName());
Map<String,Object> injectionData = new HashMap<String,Object>();
injectionData.put("dataSource", dataSource);
return ConnectionProviderFactory.newConnectionProvider(properties, injectionData);
}
}
}