/*
* Copyright (c) 2015 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.io.jdbc.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Field;
import java.net.URI;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import com.spotify.docker.client.DockerException;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.core.io.report.IOReport;
import eu.esdihumboldt.hale.common.core.io.supplier.NoStreamInputSupplier;
import eu.esdihumboldt.hale.common.core.io.supplier.NoStreamOutputSupplier;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.instance.model.InstanceCollection;
import eu.esdihumboldt.hale.common.instance.model.InstanceUtil;
import eu.esdihumboldt.hale.common.instance.model.ResourceIterator;
import eu.esdihumboldt.hale.common.instance.model.TypeFilter;
import eu.esdihumboldt.hale.common.instance.model.impl.DefaultInstanceCollection;
import eu.esdihumboldt.hale.common.schema.model.ChildDefinition;
import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.Schema;
import eu.esdihumboldt.hale.common.schema.model.TypeDefinition;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.Binding;
import eu.esdihumboldt.hale.common.schema.model.impl.DefaultSchemaSpace;
import eu.esdihumboldt.hale.common.test.TestUtil;
import eu.esdihumboldt.hale.common.test.docker.config.HaleDockerClient;
import eu.esdihumboldt.hale.io.jdbc.JDBCConnection;
import eu.esdihumboldt.hale.io.jdbc.JDBCInstanceReader;
import eu.esdihumboldt.hale.io.jdbc.JDBCInstanceWriter;
import eu.esdihumboldt.hale.io.jdbc.JDBCSchemaReader;
import eu.esdihumboldt.hale.io.jdbc.SQLSchemaReader;
import eu.esdihumboldt.hale.io.jdbc.constraints.SQLType;
/**
* Base class for database tests.
*
* @author Simon Templer
*/
public abstract class AbstractDBTest {
/**
* the config key specifying the time in seconds required for starting the
* database
*/
private final DBImageParameters dbi;
private HaleDockerClient client;
private URI jdbcUri;
/**
* @param imageParams the config parameters required while creating and
* starting the container.
*
*/
public AbstractDBTest(DBImageParameters imageParams) {
this.dbi = imageParams;
}
/**
* Create a new database test class.
*
* @param configName the name of the docker configuration to use for ramping
* up the database
* @param contextClass the class in which context (class loader) the
* configuration is defined
*/
public AbstractDBTest(String configName, Class<?> contextClass) {
this(new DBConfigInstance(configName, contextClass.getClassLoader()));
}
/**
* Setup host and database.
*
* @throws InterruptedException thrown while creating, starting or
* inspecting a container.
* @throws DockerException ImageNotFoundException if the image is not found
* while creating a container. ContainerNotFoundException if
* container is not found while starting/inspecting a container.
*/
@Before
public void setupDB() throws DockerException, InterruptedException {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
// set context class loaded to this class' class loader to be able
// to find hale-docker.conf
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
client = new HaleDockerClient(dbi);
client.createContainer();
client.startContainer();
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
String host = client.getHostName();
if (host == null) {
// using docker container directly (probably unix socket connection)
jdbcUri = URI.create(dbi.getJDBCURL(dbi.getDBPort(), client.getContainerIp()));
}
else {
jdbcUri = URI.create(dbi.getJDBCURL(client.getHostPort(dbi.getDBPort()), host));
}
TestUtil.startConversionService();
}
/**
* Wait for the database to be ready.
*
* @throws SQLException if connecting to the database fails
*/
protected void waitForDatabase() throws SQLException {
waitForConnection().close();
}
/**
* Wait for connection to database.
*
* @return the connection to the database once it is set up, the caller is
* responsible to close it
* @throws SQLException if connecting to the database fails
*/
protected Connection waitForConnection() throws SQLException {
int num = 0;
int waitTime = 240;
SQLException lastException = null;
Connection result = null;
waitTime = dbi.getStartUPTime();
System.out.print("Waiting for database to start");
while (num < waitTime) {
try {
result = JDBCConnection.getConnection(jdbcUri, dbi.getUser(), dbi.getPassword());
break;
} catch (SQLException e) {
// if (!e.getMessage().toLowerCase().contains("database")) {
// throw e;
// }
lastException = e;
}
num++;
if (num % 10 == 0) {
System.out.print(num);
}
else {
System.out.print('.');
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignore
}
}
System.out.println("...complete");
if (result != null) {
return result;
}
if (lastException != null) {
throw lastException;
}
return null;
}
/**
* Load the database schema.
*
* @return the schema
* @throws Exception if reading the schema fails
*/
protected Schema readSchema() throws Exception {
JDBCSchemaReader schemaReader = new JDBCSchemaReader();
schemaReader.setSource(new NoStreamInputSupplier(jdbcUri));
schemaReader.setParameter(JDBCSchemaReader.PARAM_USER, Value.of(dbi.getUser()));
schemaReader.setParameter(JDBCSchemaReader.PARAM_PASSWORD, Value.of(dbi.getPassword()));
// This is set for setting inclusion rule for reading schema
if (dbi.getDatabase().equalsIgnoreCase("ORCL")) {
schemaReader.setParameter(JDBCSchemaReader.SCHEMAS,
Value.of(dbi.getUser().toUpperCase()));
}
IOReport report = schemaReader.execute(null);
assertTrue(report.isSuccess());
assertTrue(report.getErrors().isEmpty());
Schema schema = schemaReader.getSchema();
assertNotNull(schema);
return schema;
}
/**
* Load the database schema for a SQL statement.
*
* @param sql the SQL query
* @param typeName the type name for the query
*
* @return the schema
* @throws Exception if reading the schema fails
*/
protected Schema readSchema(String sql, String typeName) throws Exception {
SQLSchemaReader schemaReader = new SQLSchemaReader();
schemaReader.setSource(new NoStreamInputSupplier(jdbcUri));
schemaReader.setParameter(JDBCSchemaReader.PARAM_USER, Value.of(dbi.getUser()));
schemaReader.setParameter(JDBCSchemaReader.PARAM_PASSWORD, Value.of(dbi.getPassword()));
schemaReader.setParameter(SQLSchemaReader.PARAM_SQL, Value.of(sql));
schemaReader.setParameter(SQLSchemaReader.PARAM_TYPE_NAME, Value.of(typeName));
IOReport report = schemaReader.execute(null);
assertTrue(report.isSuccess());
assertTrue(report.getErrors().isEmpty());
Schema schema = schemaReader.getSchema();
assertNotNull(schema);
return schema;
}
/**
* It checks if the binding of a data type read from schema and the expected
* binding are equal.
*
* @param map It maps a data type with a binding class to be expected. e.g.
* for postgresql db, for data type VARCHAR, the expected binding
* class is String.class
* @param schema the schema read.
* @throws Exception exception may thrown while getting the value of a
* static or instance field of type int.
*/
protected void checkBindingAndSqlType(Schema schema, Map<String, Class<?>> map)
throws Exception {
final Map<String, Integer> sqlTypeMap = new HashMap<>();
// all types fields
for (final Field f : Types.class.getFields()) {
sqlTypeMap.put(f.getName(), f.getInt(null));
}
for (TypeDefinition td : schema.getTypes()) {
for (ChildDefinition<?> cd : td.getChildren()) {
PropertyDefinition property = cd.asProperty();
String name = property.getPropertyType().getName().getLocalPart().toUpperCase();
SQLType t = property.getPropertyType().getConstraint(SQLType.class);
assertTrue(sqlTypeMap.containsValue(new Integer(t.getType())));
Binding k = property.getPropertyType().getConstraint(Binding.class);
// check bindings for those data type for which expected binding
// is mapped.
if (map.containsKey(name))
assertEquals(map.get(name), k.getBinding());
else
fail(MessageFormat.format(
"No expected binding specified for type {0} (SQL type {1}) - binding is {2}",
name, t.getType(), k.getBinding()));
}
}
}
/**
* Write instances to the database.
*
* @param instances the collection of instances
* @param schema the target schema
* @throws Exception if writing the instances fails
*/
protected void writeInstances(InstanceCollection instances, Schema schema) throws Exception {
JDBCInstanceWriter writer = new JDBCInstanceWriter();
writer.setTarget(new NoStreamOutputSupplier(jdbcUri));
writer.setParameter(JDBCInstanceWriter.PARAM_USER, Value.of(dbi.getUser()));
writer.setParameter(JDBCInstanceWriter.PARAM_PASSWORD, Value.of(dbi.getPassword()));
writer.setInstances(instances);
DefaultSchemaSpace targetSchema = new DefaultSchemaSpace();
targetSchema.addSchema(schema);
writer.setTargetSchema(targetSchema);
IOReport report = writer.execute(null);
assertTrue(report.isSuccess());
assertTrue(report.getErrors().isEmpty());
}
/**
* Read instances from the database.
*
* @param schema the source schema
* @return the database instances
*
* @throws Exception if reading the instances fails
*/
protected InstanceCollection readInstances(Schema schema) throws Exception {
JDBCInstanceReader reader = new JDBCInstanceReader();
reader.setSource(new NoStreamInputSupplier(jdbcUri));
reader.setParameter(JDBCInstanceWriter.PARAM_USER, Value.of(dbi.getUser()));
reader.setParameter(JDBCInstanceWriter.PARAM_PASSWORD, Value.of(dbi.getPassword()));
DefaultSchemaSpace sourceSchema = new DefaultSchemaSpace();
sourceSchema.addSchema(schema);
reader.setSourceSchema(sourceSchema);
IOReport report = reader.execute(null);
assertTrue(report.isSuccess());
assertTrue(report.getErrors().isEmpty());
return reader.getInstances();
}
/**
* Read the instances from the db, check if it is same as instances written
* to the db.
*
* @param originalInstances instance created and written to db
* @param schema schema read
* @param gType the geometry type definition.
* @return The count of instances which are equal to the original instances
* @throws Exception if reading the instances fails.
*/
protected int readAndCountInstances(InstanceCollection originalInstances, Schema schema,
TypeDefinition gType) throws Exception {
InstanceCollection instancesRead = readInstances(schema).select(new TypeFilter(gType));
List<Instance> originals = new DefaultInstanceCollection(originalInstances).toList();
ResourceIterator<Instance> ri = instancesRead.iterator();
int count = 0;
try {
while (ri.hasNext()) {
Instance instance = ri.next();
String error = InstanceUtil.checkInstance(instance, originals);
assertNull(error, error);
count++;
}
} finally {
ri.close();
}
return count;
}
/**
* stop and remove the container.
*
* @throws Exception if killing and removing the container fail.
*/
@After
public void tearDownDocker() throws Exception {
client.killAndRemoveContainer();
}
}