package liquibase.integration.servlet;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.configuration.*;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.logging.LogFactory;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.CompositeResourceAccessor;
import liquibase.resource.FileSystemResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.util.NetUtil;
import liquibase.util.StringUtils;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
/**
* Servlet listener than can be added to web.xml to allow Liquibase to run on every application server startup.
* Using this listener allows users to know that they always have the most up to date database, although it will
* slow down application server startup slightly.
* See the <a href="http://www.liquibase.org/documentation/servlet_listener.html">Liquibase documentation</a> for
* more information.
*/
public class LiquibaseServletListener implements ServletContextListener {
private static final String JAVA_COMP_ENV = "java:comp/env";
private static final String LIQUIBASE_CHANGELOG = "liquibase.changelog";
private static final String LIQUIBASE_CONTEXTS = "liquibase.contexts";
private static final String LIQUIBASE_LABELS = "liquibase.labels";
private static final String LIQUIBASE_DATASOURCE = "liquibase.datasource";
private static final String LIQUIBASE_HOST_EXCLUDES = "liquibase.host.excludes";
private static final String LIQUIBASE_HOST_INCLUDES = "liquibase.host.includes";
private static final String LIQUIBASE_ONERROR_FAIL = "liquibase.onerror.fail";
private static final String LIQUIBASE_PARAMETER = "liquibase.parameter";
private static final String LIQUIBASE_SCHEMA_DEFAULT = "liquibase.schema.default";
private String changeLogFile;
private String dataSourceName;
private String contexts;
private String labels;
private String defaultSchema;
private String hostName;
private ServletValueContainer servletValueContainer; //temporarily saved separately until all lookup moves to liquibaseConfiguration
public String getChangeLogFile() {
return changeLogFile;
}
public void setContexts(String ctxt) {
contexts = ctxt;
}
public String getContexts() {
return contexts;
}
public String getLabels() {
return labels;
}
public void setLabels(String labels) {
this.labels = labels;
}
public void setChangeLogFile(String changeLogFile) {
this.changeLogFile = changeLogFile;
}
public String getDataSource() {
return dataSourceName;
}
public String getDefaultSchema() {
return defaultSchema;
}
/**
* Sets the name of the data source.
*/
public void setDataSource(String dataSource) {
this.dataSourceName = dataSource;
}
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
ServletContext servletContext = servletContextEvent.getServletContext();
try {
this.hostName = NetUtil.getLocalHostName();
}
catch (Exception e) {
servletContext.log("Cannot find hostname: " + e.getMessage());
return;
}
InitialContext ic = null;
String failOnError = null;
try {
ic = new InitialContext();
servletValueContainer = new ServletValueContainer(servletContext, ic);
LiquibaseConfiguration.getInstance().init(servletValueContainer);
failOnError = (String) servletValueContainer.getValue(LIQUIBASE_ONERROR_FAIL);
if (checkPreconditions(servletContext, ic)) {
executeUpdate(servletContext, ic);
}
} catch (Exception e) {
if (!"false".equals(failOnError)) {
throw new RuntimeException(e);
}
} finally {
if (ic != null) {
try {
ic.close();
}
catch (NamingException e) {
// ignore
}
}
}
}
/**
* Checks if the update is supposed to be executed. That depends on several conditions:
* <ol>
* <li>if liquibase.shouldRun is <code>false</code> the update will not be executed.</li>
* <li>if {@value LiquibaseServletListener#LIQUIBASE_HOST_INCLUDES} contains the current hostname, the the update will be executed.</li>
* <li>if {@value LiquibaseServletListener#LIQUIBASE_HOST_EXCLUDES} contains the current hostname, the the update will not be executed.</li>
* </ol>
*/
private boolean checkPreconditions(ServletContext servletContext, InitialContext ic) {
GlobalConfiguration globalConfiguration = LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class);
if (!globalConfiguration.getShouldRun()) {
LogFactory.getLogger().info( "Liquibase did not run on " + hostName
+ " because "+ LiquibaseConfiguration.getInstance().describeValueLookupLogic(globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN))
+ " was set to false");
return false;
}
String machineIncludes = (String) servletValueContainer.getValue(LIQUIBASE_HOST_INCLUDES);
String machineExcludes = (String) servletValueContainer.getValue(LIQUIBASE_HOST_EXCLUDES);
boolean shouldRun = false;
if (machineIncludes == null && machineExcludes == null) {
shouldRun = true;
} else if (machineIncludes != null) {
for (String machine : machineIncludes.split(",")) {
machine = machine.trim();
if (hostName.equalsIgnoreCase(machine)) {
shouldRun = true;
}
}
} else if (machineExcludes != null) {
shouldRun = true;
for (String machine : machineExcludes.split(",")) {
machine = machine.trim();
if (hostName.equalsIgnoreCase(machine)) {
shouldRun = false;
}
}
}
if (globalConfiguration.getShouldRun() && globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN).getWasOverridden()) {
shouldRun = true;
servletContext.log("ignoring " + LIQUIBASE_HOST_INCLUDES + " and "
+ LIQUIBASE_HOST_EXCLUDES + ", since " + LiquibaseConfiguration.getInstance().describeValueLookupLogic(globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN))
+ "=true");
}
if (!shouldRun) {
servletContext.log("LiquibaseServletListener did not run due to "
+ LIQUIBASE_HOST_INCLUDES + " and/or " + LIQUIBASE_HOST_EXCLUDES + "");
return false;
}
return true;
}
/**
* Executes the Liquibase update.
*/
private void executeUpdate(ServletContext servletContext, InitialContext ic) throws NamingException, SQLException, LiquibaseException {
setDataSource((String) servletValueContainer.getValue(LIQUIBASE_DATASOURCE));
if (getDataSource() == null) {
throw new RuntimeException("Cannot run Liquibase, " + LIQUIBASE_DATASOURCE + " is not set");
}
setChangeLogFile((String) servletValueContainer.getValue(LIQUIBASE_CHANGELOG));
if (getChangeLogFile() == null) {
throw new RuntimeException("Cannot run Liquibase, " + LIQUIBASE_CHANGELOG + " is not set");
}
setContexts((String) servletValueContainer.getValue(LIQUIBASE_CONTEXTS));
setLabels((String) servletValueContainer.getValue(LIQUIBASE_LABELS));
this.defaultSchema = StringUtils.trimToNull((String) servletValueContainer.getValue(LIQUIBASE_SCHEMA_DEFAULT));
Connection connection = null;
Database database = null;
try {
DataSource dataSource = (DataSource) ic.lookup(this.dataSourceName);
connection = dataSource.getConnection();
Thread currentThread = Thread.currentThread();
ClassLoader contextClassLoader = currentThread.getContextClassLoader();
ResourceAccessor threadClFO = new ClassLoaderResourceAccessor(contextClassLoader);
ResourceAccessor clFO = new ClassLoaderResourceAccessor();
ResourceAccessor fsFO = new FileSystemResourceAccessor();
database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
database.setDefaultSchemaName(getDefaultSchema());
Liquibase liquibase = new Liquibase(getChangeLogFile(), new CompositeResourceAccessor(clFO, fsFO, threadClFO), database);
@SuppressWarnings("unchecked")
Enumeration<String> initParameters = servletContext.getInitParameterNames();
while (initParameters.hasMoreElements()) {
String name = initParameters.nextElement().trim();
if (name.startsWith(LIQUIBASE_PARAMETER + ".")) {
liquibase.setChangeLogParameter(name.substring(LIQUIBASE_PARAMETER.length()), servletValueContainer.getValue(name));
}
}
liquibase.update(new Contexts(getContexts()), new LabelExpression(getLabels()));
}
finally {
if (database != null) {
database.close();
} else if (connection != null) {
connection.close();
}
}
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
protected class ServletValueContainer implements ConfigurationValueProvider {
private ServletContext servletContext;
private InitialContext initialContext;
public ServletValueContainer(ServletContext servletContext, InitialContext initialContext) {
this.servletContext = servletContext;
this.initialContext = initialContext;
}
@Override
public String describeValueLookupLogic(ConfigurationProperty property) {
return "JNDI, servlet container init parameter, and system property '"+property.getNamespace()+"."+property.getName()+"'";
}
@Override
public Object getValue(String namespace, String property) {
return getValue(namespace +"."+property);
}
/**
* Try to read the value that is stored by the given key from
* <ul>
* <li>JNDI</li>
* <li>the servlet context's init parameters</li>
* <li>system properties</li>
* </ul>
*/
public Object getValue(String prefixAndProperty) {
// Try to get value from JNDI
try {
Context envCtx = (Context) initialContext.lookup(JAVA_COMP_ENV);
String valueFromJndi = (String) envCtx.lookup(prefixAndProperty);
return valueFromJndi;
}
catch (NamingException e) {
// Ignore
}
// Return the value from the servlet context
String valueFromServletContext = servletContext.getInitParameter(prefixAndProperty);
if (valueFromServletContext != null) {
return valueFromServletContext;
}
// Otherwise: Return system property
return System.getProperty(prefixAndProperty);
}
}
}