/**
* Licensed to JumpMind Inc under one or more contributor
* license agreements. See the NOTICE file distributed
* with this work for additional information regarding
* copyright ownership. JumpMind Inc licenses this file
* to you under the GNU General Public License, version 3.0 (GPLv3)
* (the "License"); you may not use this file except in compliance
* with the License.
*
* You should have received a copy of the GNU General Public License,
* version 3.0 (GPLv3) along with this library; if not, see
* <http://www.gnu.org/licenses/>.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jumpmind.symmetric;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import java.io.File;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.jumpmind.db.platform.IDatabasePlatform;
import org.jumpmind.db.platform.JdbcDatabasePlatformFactory;
import org.jumpmind.db.sql.JdbcSqlTemplate;
import org.jumpmind.db.sql.SqlTemplateSettings;
import org.jumpmind.db.util.BasicDataSourceFactory;
import org.jumpmind.properties.TypedProperties;
import org.jumpmind.security.SecurityServiceFactory;
import org.jumpmind.security.SecurityServiceFactory.SecurityServiceType;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.common.SystemConstants;
import org.jumpmind.symmetric.db.ISymmetricDialect;
import org.jumpmind.symmetric.db.JdbcSymmetricDialectFactory;
import org.jumpmind.symmetric.io.stage.IStagingManager;
import org.jumpmind.symmetric.io.stage.StagingManager;
import org.jumpmind.symmetric.job.IJobManager;
import org.jumpmind.symmetric.job.JobManager;
import org.jumpmind.symmetric.service.IExtensionService;
import org.jumpmind.symmetric.service.impl.ClientExtensionService;
import org.jumpmind.symmetric.util.LogSummaryAppenderUtils;
import org.jumpmind.symmetric.util.SnapshotUtil;
import org.jumpmind.symmetric.util.TypedPropertiesFactory;
import org.jumpmind.util.AppUtils;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jndi.JndiObjectFactoryBean;
import org.xml.sax.InputSource;
/**
* Represents the client portion of a SymmetricDS engine. This class can be used
* to embed SymmetricDS into another application.
*/
public class ClientSymmetricEngine extends AbstractSymmetricEngine {
public static final String DEPLOYMENT_TYPE_CLIENT = "client";
public static final String PROPERTIES_FACTORY_CLASS_NAME = "properties.factory.class.name";
protected File propertiesFile;
protected Properties properties;
protected DataSource dataSource;
protected ApplicationContext springContext;
/**
* @param dataSource
* If not null, SymmetricDS will use this provided datasource
* instead of creating it's own.
* @param springContext
* If not null, SymmetricDS will use this provided Spring context
* instead of creating it's own.
* @param properties
* Properties to use for configuration.
* @param registerEngine
* Whether to store a reference to this engine in a local static
* map.
*/
public ClientSymmetricEngine(DataSource dataSource, ApplicationContext springContext,
Properties properties, boolean registerEngine) {
super(registerEngine);
setDeploymentType(DEPLOYMENT_TYPE_CLIENT);
this.dataSource = dataSource;
this.springContext = springContext;
this.properties = properties;
this.init();
}
public ClientSymmetricEngine(DataSource dataSource, Properties properties,
boolean registerEngine) {
super(registerEngine);
setDeploymentType(DEPLOYMENT_TYPE_CLIENT);
this.dataSource = dataSource;
this.properties = properties;
this.init();
}
public ClientSymmetricEngine(File propertiesFile, boolean registerEngine) {
super(registerEngine);
setDeploymentType(DEPLOYMENT_TYPE_CLIENT);
this.propertiesFile = propertiesFile;
this.init();
}
public ClientSymmetricEngine(File propertiesFile) {
this(propertiesFile, true);
}
public ClientSymmetricEngine(File propertiesFile, ApplicationContext springContext) {
super(true);
setDeploymentType(DEPLOYMENT_TYPE_CLIENT);
this.propertiesFile = propertiesFile;
this.springContext = springContext;
this.init();
}
public ClientSymmetricEngine(Properties properties, boolean registerEngine) {
super(registerEngine);
setDeploymentType(DEPLOYMENT_TYPE_CLIENT);
this.properties = properties;
this.init();
}
public ClientSymmetricEngine(Properties properties) {
this(properties, true);
}
public ClientSymmetricEngine() {
this((Properties) null, true);
}
public ClientSymmetricEngine(boolean registerEngine) {
this((Properties) null, registerEngine);
}
@Override
protected SecurityServiceType getSecurityServiceType() {
return SecurityServiceType.CLIENT;
}
@Override
protected void init() {
try {
LogSummaryAppenderUtils.registerLogSummaryAppender();
super.init();
this.dataSource = platform.getDataSource();
PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
configurer.setProperties(parameterService.getAllParameters());
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(springContext);
ctx.addBeanFactoryPostProcessor(configurer);
List<String> extensionLocations = new ArrayList<String>();
extensionLocations.add("classpath:/symmetric-ext-points.xml");
if (registerEngine) {
extensionLocations.add("classpath:/symmetric-jmx.xml");
}
String xml = parameterService.getString(ParameterConstants.EXTENSIONS_XML);
File file = new File(parameterService.getTempDirectory(), "extension.xml");
FileUtils.deleteQuietly(file);
if (isNotBlank(xml)) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
// the "parse" method also validates XML, will throw an exception if misformatted
builder.parse(new InputSource(new StringReader(xml)));
FileUtils.write(file, xml, false);
extensionLocations.add("file:" + file.getAbsolutePath());
} catch (Exception e) {
log.error("Invalid " + ParameterConstants.EXTENSIONS_XML + " parameter.");
}
}
try {
ctx.setConfigLocations(extensionLocations.toArray(new String[extensionLocations
.size()]));
ctx.refresh();
this.springContext = ctx;
((ClientExtensionService) this.extensionService).setSpringContext(springContext);
this.extensionService.refresh();
} catch (Exception ex) {
log.error(
"Failed to initialize the extension points. Please fix the problem and restart the server.",
ex);
}
} catch (RuntimeException ex) {
destroy();
throw ex;
}
}
@Override
public synchronized boolean start() {
if (this.springContext instanceof AbstractApplicationContext) {
AbstractApplicationContext ctx = (AbstractApplicationContext) this.springContext;
try {
if (!ctx.isActive()) {
ctx.start();
}
} catch (Exception ex) {
}
}
return super.start();
}
@Override
public synchronized void stop() {
if (this.springContext instanceof AbstractApplicationContext) {
AbstractApplicationContext ctx = (AbstractApplicationContext) this.springContext;
try {
if (ctx.isActive()) {
ctx.stop();
}
} catch (Exception ex) {
}
}
super.stop();
}
public static BasicDataSource createBasicDataSource(File propsFile) {
TypedProperties properties = createTypedPropertiesFactory(propsFile, null).reload();
return BasicDataSourceFactory.create(properties, SecurityServiceFactory.create(SecurityServiceType.CLIENT, properties));
}
@Override
protected ISymmetricDialect createSymmetricDialect() {
return new JdbcSymmetricDialectFactory(parameterService, platform).create();
}
@Override
protected IDatabasePlatform createDatabasePlatform(TypedProperties properties) {
IDatabasePlatform platform = createDatabasePlatform(springContext, properties, dataSource,
Boolean.parseBoolean(System.getProperty(SystemConstants.SYSPROP_WAIT_FOR_DATABASE, "true")));
return platform;
}
public static IDatabasePlatform createDatabasePlatform(ApplicationContext springContext, TypedProperties properties,
DataSource dataSource, boolean waitOnAvailableDatabase) {
if (dataSource == null) {
String jndiName = properties.getProperty(ParameterConstants.DB_JNDI_NAME);
if (StringUtils.isNotBlank(jndiName)) {
try {
log.info("Looking up datasource in jndi. The jndi name is {}", jndiName);
JndiObjectFactoryBean jndiFactory = new JndiObjectFactoryBean();
jndiFactory.setJndiName(jndiName);
jndiFactory.afterPropertiesSet();
dataSource = (DataSource)jndiFactory.getObject();
if (dataSource == null) {
throw new SymmetricException("Could not locate the configured datasource in jndi. The jndi name is %s", jndiName);
}
} catch (IllegalArgumentException e) {
throw new SymmetricException("Could not locate the configured datasource in jndi. The jndi name is %s", e, jndiName);
} catch (NamingException e) {
throw new SymmetricException("Could not locate the configured datasource in jndi. The jndi name is %s", e, jndiName);
}
}
String springBeanName = properties.getProperty(ParameterConstants.DB_SPRING_BEAN_NAME);
if (isNotBlank(springBeanName) && springContext != null) {
log.info("Using datasource from spring. The spring bean name is {}", springBeanName);
dataSource = (DataSource)springContext.getBean(springBeanName);
}
if (dataSource == null) {
dataSource = BasicDataSourceFactory.create(properties, SecurityServiceFactory.create(SecurityServiceType.CLIENT, properties));
}
}
if (waitOnAvailableDatabase) {
waitForAvailableDatabase(dataSource);
}
boolean delimitedIdentifierMode = properties.is(
ParameterConstants.DB_DELIMITED_IDENTIFIER_MODE, true);
return JdbcDatabasePlatformFactory.createNewPlatformInstance(dataSource,
createSqlTemplateSettings(properties), delimitedIdentifierMode);
}
protected static SqlTemplateSettings createSqlTemplateSettings(TypedProperties properties) {
SqlTemplateSettings settings = new SqlTemplateSettings();
settings.setFetchSize(properties.getInt(ParameterConstants.DB_FETCH_SIZE, 1000));
settings.setQueryTimeout(properties.getInt(ParameterConstants.DB_QUERY_TIMEOUT_SECS, 300));
settings.setBatchSize(properties.getInt(ParameterConstants.JDBC_EXECUTE_BATCH_SIZE, 100));
settings.setReadStringsAsBytes(properties.is(ParameterConstants.JDBC_READ_STRINGS_AS_BYTES, false));
return settings;
}
@Override
protected IExtensionService createExtensionService() {
return new ClientExtensionService(this, springContext);
}
@Override
protected IJobManager createJobManager() {
return new JobManager(this);
}
@Override
protected IStagingManager createStagingManager() {
String directory = parameterService.getTempDirectory();
return new StagingManager(directory);
}
protected static void waitForAvailableDatabase(DataSource dataSource) {
boolean success = false;
while (!success) {
Connection c = null;
try {
synchronized (ClientSymmetricEngine.class) {
c = dataSource.getConnection();
success = true;
}
} catch (Exception ex) {
log.error(
"Could not get a connection to the database: {}. Waiting for 10 seconds before trying to connect to the database again.",
ex.getMessage());
AppUtils.sleep(10000);
} finally {
JdbcSqlTemplate.close(c);
}
}
}
@Override
protected ITypedPropertiesFactory createTypedPropertiesFactory() {
return createTypedPropertiesFactory(propertiesFile, properties);
}
protected static ITypedPropertiesFactory createTypedPropertiesFactory(File propFile, Properties prop) {
String propFactoryClassName = System.getProperties().getProperty(PROPERTIES_FACTORY_CLASS_NAME);
ITypedPropertiesFactory factory = null;
if (propFactoryClassName != null) {
try {
factory = (ITypedPropertiesFactory) Class.forName(propFactoryClassName).newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
factory = new TypedPropertiesFactory();
}
factory.init(propFile, prop);
return factory;
}
@Override
public synchronized void destroy() {
super.destroy();
if (springContext instanceof AbstractApplicationContext) {
try {
((AbstractApplicationContext)springContext).destroy();
} catch (Exception ex) {
}
}
springContext = null;
if (dataSource != null && dataSource instanceof BasicDataSource) {
try {
((BasicDataSource)dataSource).close();
} catch (SQLException e) {
}
}
}
public List<File> listSnapshots() {
File snapshotsDir = SnapshotUtil.getSnapshotDirectory(this);
List<File> files = new ArrayList<File>(FileUtils.listFiles(snapshotsDir, new String[] {"zip"}, false));
Collections.sort(files, new Comparator<File>() {
public int compare(File o1, File o2) {
return -o1.compareTo(o2);
}
});
return files;
}
public ApplicationContext getSpringContext() {
return springContext;
}
public File snapshot() {
return SnapshotUtil.createSnapshot(this);
}
}