You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
15 KiB
317 lines
15 KiB
package de.superx.kettle; |
|
|
|
import java.io.File; |
|
import java.io.FileOutputStream; |
|
import java.io.IOException; |
|
import java.io.OutputStream; |
|
import java.io.OutputStreamWriter; |
|
import java.io.Writer; |
|
import java.nio.charset.StandardCharsets; |
|
import java.sql.Connection; |
|
import java.sql.ResultSet; |
|
import java.sql.SQLException; |
|
import java.sql.Statement; |
|
import java.time.Year; |
|
import java.util.ArrayList; |
|
import java.util.LinkedList; |
|
import java.util.List; |
|
|
|
import javax.naming.NamingException; |
|
import javax.sql.DataSource; |
|
import javax.xml.namespace.NamespaceContext; |
|
import javax.xml.parsers.ParserConfigurationException; |
|
import javax.xml.transform.OutputKeys; |
|
import javax.xml.transform.Source; |
|
import javax.xml.transform.Transformer; |
|
import javax.xml.transform.TransformerFactory; |
|
import javax.xml.transform.dom.DOMSource; |
|
import javax.xml.transform.stream.StreamResult; |
|
import javax.xml.validation.Schema; |
|
import javax.xml.validation.SchemaFactory; |
|
import javax.xml.validation.Validator; |
|
import javax.xml.xpath.XPath; |
|
import javax.xml.xpath.XPathConstants; |
|
import javax.xml.xpath.XPathExpression; |
|
import javax.xml.xpath.XPathExpressionException; |
|
import javax.xml.xpath.XPathFactory; |
|
|
|
import org.apache.xerces.jaxp.validation.XMLSchemaFactory; |
|
import org.junit.AfterClass; |
|
//import org.apache.xerces.jaxp.validation.XMLSchemaFactory; |
|
//import org.h2.tools.Server; |
|
import org.junit.Assert; |
|
import org.junit.Test; |
|
import org.pentaho.di.core.KettleEnvironment; |
|
import org.pentaho.di.core.Result; |
|
import org.pentaho.di.core.database.DataSourceProviderFactory; |
|
import org.pentaho.di.core.database.DataSourceProviderInterface; |
|
import org.pentaho.di.core.database.DatabaseMeta; |
|
import org.pentaho.di.core.exception.KettleException; |
|
import org.pentaho.di.core.exception.KettleXMLException; |
|
import org.pentaho.di.core.logging.LogLevel; |
|
import org.pentaho.di.core.parameters.UnknownParamException; |
|
import org.pentaho.di.job.Job; |
|
import org.pentaho.di.job.JobMeta; |
|
import org.pentaho.di.metastore.DatabaseMetaStoreUtil; |
|
import org.pentaho.metastore.api.exceptions.MetaStoreException; |
|
import org.pentaho.metastore.stores.memory.MemoryMetaStore; |
|
import org.w3c.dom.Document; |
|
import org.w3c.dom.Node; |
|
import org.w3c.dom.NodeList; |
|
import org.xml.sax.SAXException; |
|
|
|
import de.memtext.util.StringUtils; |
|
import de.memtext.util.XMLUtils; |
|
import de.superx.db.H2InMemoryCreator; |
|
import de.superx.util.test.KdsfNamespaceContext; |
|
import de.superx.util.test.KettleH2TestDataSourceProvider; |
|
|
|
public class KdsfTransformationTest { |
|
|
|
private static final int YEAR_OF_REFERENCE_DATA_CREATION = 2019; |
|
private static final String SCHEMA_PATH = String.join(File.separator, "test", "resources", "schema_files", "kdsf_xml_export", "schema"); |
|
private static final String VIEWS_PATH = String.join(File.separator, "test", "resources", "schema_files", "kdsf_xml_export", "views"); |
|
private static final String UNLOADS_PATH = String.join(File.separator, "test", "resources", "schema_files", "kdsf_xml_export", "unloads"); |
|
private static final String SCHEMA_LOCATION = String.join(File.separator, "test", "resources", "schema_files", "kdsf_xml_export", "xsd", "kdsf.xsd"); |
|
private static final String KETTLE_PLUGIN_DIR = String.join(File.separator, "superx", "WEB-INF", "kettle-plugins"); |
|
|
|
private static final String REFERENCE_FILE_LOCATION_BASE = String.join(File.separator, "test", "resources", "xmlunittest", "kdsf_testfile_"); |
|
|
|
private DataSource datasource = null; |
|
private List<String> errorList; |
|
|
|
@Test |
|
public void testKdsfTransformation() throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException, SQLException, NamingException, KettleException, MetaStoreException, XPathExpressionException, ParserConfigurationException, SAXException { |
|
createInMemoryTestDb(); |
|
MemoryMetaStore metastore = new MemoryMetaStore(); |
|
initKettleEnvironment(metastore); |
|
this.errorList = new ArrayList<String>(); |
|
|
|
// attention: it seems that in memory datasource is destroyed by kettle processing if we do not keep an open connection!? |
|
try ( Connection conn = this.datasource.getConnection(); ) { |
|
verjuengeTestdaten(conn); |
|
exportAndValidateYear(metastore, conn, 2018); |
|
exportAndValidateYear(metastore, conn, 1900); |
|
// Server.startWebServer(conn); // uncomment for local (e.g. dev env) debugging |
|
} |
|
|
|
Assert.assertEquals(String.join("\n", this.errorList), 0, this.errorList.size()); |
|
} |
|
|
|
public static void main(String args[]) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException { |
|
if (args.length == 0) { |
|
printHelpAndExit(); |
|
} else if (args.length < 2) { |
|
System.out.println("ERROR: awaiting two input files..."); |
|
System.exit(-1); |
|
} |
|
String referenceFilePath = args[0]; |
|
String testFilePath = args[1]; |
|
compareXmlLeaveElements(referenceFilePath, testFilePath); |
|
} |
|
|
|
private static void compareXmlLeaveElements(String referenceFilePath, String testFilePath) |
|
throws IOException, ParserConfigurationException, SAXException, XPathExpressionException { |
|
Document referenceDocument = loadXMLDocument(referenceFilePath); |
|
Document testDocument = loadXMLDocument(testFilePath); |
|
validateAgainstSchema(referenceDocument); |
|
validateAgainstSchema(testDocument); |
|
List<String> missingTestLeaves = findMissingLeaves(referenceDocument, testDocument); |
|
if (!missingTestLeaves.isEmpty()) { |
|
System.out.println("the following elements from reference file are missing in export file\n" + |
|
String.join("\n", missingTestLeaves)); |
|
} |
|
} |
|
|
|
private static void printHelpAndExit() { |
|
System.out.println("Use this program to perform a basic check on exported KDSF-Files:\n" |
|
+ "- the first parameter is used as path for a reference file\n" |
|
+ "- the second parameter is used as path for the file to be checked\n" |
|
+ "- if both files conform to the KDSF XSD schema\n" |
|
+ "- and if the XML leave elements of the reference file are contained in those of the file to be checked\n" |
|
+ "...then the test is cosidered to be successful"); |
|
System.exit(0); |
|
} |
|
|
|
/* |
|
* Im KDSF-Export wird über eine Formel die Altersgruppe ermittelt; |
|
* Da die Referenzdatei des Tests statisch ist, muss das Alter in den Testdaten verjüngt werden, |
|
* damit sich die Altersgruppe nicht ändert, wenn der Testfall zu einem späteren Zeitpunkt |
|
* ausgeführt wird (analog zu Datenverjüngung in HISinOne). |
|
*/ |
|
private static void verjuengeTestdaten(Connection conn) throws SQLException { |
|
Statement statement = conn.createStatement(); |
|
int birthyearOffset = Year.now().getValue() - YEAR_OF_REFERENCE_DATA_CREATION; |
|
statement.execute("UPDATE res_person SET birthdate = DATEADD(YEAR, " + birthyearOffset + ", birthdate);"); |
|
} |
|
|
|
private void exportAndValidateYear( MemoryMetaStore metastore, Connection conn, int year) throws SQLException, UnknownParamException, KettleXMLException, ParserConfigurationException, SAXException, IOException, XPathExpressionException { |
|
|
|
Job job = runKettleJob(metastore, year); |
|
notifyLoggedErrors(job); |
|
String exportContent = fetchXmlString(conn); |
|
Document exportDocument = XMLUtils.parseXml(exportContent); |
|
// writeToFile(exportDocument, year); // uncomment for debugging purposes |
|
validateAgainstSchema(exportDocument); |
|
// content check: ensure all xml leaves of reference file to appear in export file (naive, but should suffice) |
|
Document referenceDocument = loadXMLDocument(REFERENCE_FILE_LOCATION_BASE + year + ".xml"); |
|
validateAgainstSchema(referenceDocument); |
|
List<String> missingExportLeaves = findMissingLeaves(referenceDocument, exportDocument); |
|
|
|
if(!missingExportLeaves.isEmpty()) { |
|
this.errorList.add("the following elements from reference file are missing in export file from year " + year + "\n" + String.join("\n", missingExportLeaves)); |
|
} |
|
|
|
} |
|
|
|
private static List<String> findMissingLeaves(Document referenceDocument, Document testDocument) throws XPathExpressionException { |
|
NodeList exportElements = extractLeaveElements(testDocument); |
|
NodeList referenceElements = extractLeaveElements(referenceDocument); |
|
List<String> missingExportLeaves = findMissingMatches(referenceElements, exportElements); |
|
return missingExportLeaves; |
|
} |
|
|
|
private static List<Node> toGenericList(NodeList nodeList) { |
|
List<Node> result = new ArrayList<Node>(); |
|
for (int i = 0; i < nodeList.getLength(); i++) { |
|
result.add(nodeList.item(i)); |
|
} |
|
return result; |
|
} |
|
|
|
private static Document loadXMLDocument(String fileLocation) |
|
throws IOException, ParserConfigurationException, SAXException { |
|
File file = new File(fileLocation); |
|
String content = StringUtils.readFile(file, StandardCharsets.UTF_8); |
|
Document xmlDocument = XMLUtils.parseXml(content); |
|
return xmlDocument; |
|
} |
|
|
|
private static List<String> findMissingMatches(NodeList referenceElements, NodeList exportElements) { |
|
List<String> missingMatches = new LinkedList<String>(); |
|
List<Node> possibleMatches = toGenericList(exportElements); |
|
for(int i = 0; i < referenceElements.getLength(); i++) { |
|
Node refNode = referenceElements.item(i); |
|
if (!hasMatch(refNode, possibleMatches)) { |
|
String missedMatch = "element name: \"" + refNode.getNodeName() + "\" content: \"" + refNode.getTextContent() + "\""; |
|
missingMatches.add(missedMatch); |
|
} |
|
} |
|
return missingMatches; |
|
} |
|
|
|
private static boolean hasMatch(Node testNode, List<Node> possibleMatches) { |
|
for (int j = 0; j < possibleMatches.size(); j++) { |
|
Node matchTestNode = possibleMatches.get(j); |
|
if (testNode.isEqualNode(matchTestNode)) { |
|
// if we match one leave in the test file, we "burn" it, so it does not cause multiple matches for reference elements |
|
possibleMatches.remove(j); |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
private static NodeList extractLeaveElements(Document document) throws XPathExpressionException { |
|
XPath xpath = setupXPath(document); |
|
XPathExpression expr = xpath.compile("//*[count(./*) = 0]"); |
|
NodeList leaveElements = (NodeList)expr.evaluate(document, XPathConstants.NODESET); |
|
return leaveElements; |
|
} |
|
|
|
private static void createInMemoryTestDb() throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException, NamingException { |
|
H2InMemoryCreator h2InMemoryCreator = new H2InMemoryCreator(); |
|
String schemaPath = new File(SCHEMA_PATH).getAbsolutePath(); |
|
String viewsPath = new File(VIEWS_PATH).getAbsolutePath(); |
|
String unloadsPath = new File(UNLOADS_PATH).getAbsolutePath(); |
|
h2InMemoryCreator.setSchemaDir(schemaPath) |
|
.setViewDir(viewsPath) |
|
.setUnlDir(unloadsPath) |
|
.setUserName("sa") |
|
.setTablePattern("res_.*") |
|
.setViewPattern("skipeverything"); |
|
h2InMemoryCreator.createH2InMemory(); |
|
} |
|
|
|
private void initKettleEnvironment(MemoryMetaStore metastore) throws KettleException, MetaStoreException { |
|
System.setProperty("BI_KETTLE_PLUGIN_BASE_FOLDERS", KETTLE_PLUGIN_DIR); |
|
KettleEnvironment.init(); |
|
DataSourceProviderInterface providerinterface = new KettleH2TestDataSourceProvider("default", null); |
|
DataSourceProviderFactory.setDataSourceProviderInterface(providerinterface); |
|
this.datasource = providerinterface.getNamedDataSource("eduetl"); |
|
DatabaseMeta dbmeta = new DatabaseMeta("eduetl", "POSTGRESQL", "JNDI", null, "eduetl", "1521", null, null); |
|
DatabaseMetaStoreUtil.createDatabaseElement(metastore, dbmeta); |
|
} |
|
|
|
private static Job runKettleJob(MemoryMetaStore metastore, int year) throws KettleXMLException, UnknownParamException { |
|
JobMeta jobMeta = new JobMeta(null, "superx/WEB-INF/conf/edustore/db/module/res/etl/kdsf/job_kdsf.kjb", null, metastore, null); |
|
jobMeta.setParameterValue("release_year", "" + year); |
|
jobMeta.setParameterValue("table_prefix", "res_"); |
|
jobMeta.setParameterValue("export_employment", "1"); |
|
jobMeta.setParameterValue("export_patent", "1"); |
|
jobMeta.setParameterValue("export_project", "1"); |
|
jobMeta.setParameterValue("export_publication", "1"); |
|
jobMeta.setParameterValue("export_research_infrastructure", "1"); |
|
jobMeta.setParameterValue("export_sdcp", "1"); |
|
jobMeta.setParameterValue("export_file_name", " "); |
|
Job job = new Job(null, jobMeta); |
|
job.setLogLevel(LogLevel.ERROR); |
|
job.start(); |
|
job.waitUntilFinished(); |
|
return job; |
|
} |
|
|
|
private static void notifyLoggedErrors(Job job) { |
|
Result result = job.getResult(); |
|
String log = result.getLogText(); |
|
boolean errorsOccured = log.contains("ERROR"); |
|
Assert.assertFalse("Errors where found in kettle log:\n" + log, errorsOccured); |
|
} |
|
|
|
private static XPath setupXPath(Document document) { |
|
XPathFactory xPathfactory = XPathFactory.newInstance(); |
|
XPath xpath = xPathfactory.newXPath(); |
|
NamespaceContext nsc = new KdsfNamespaceContext(document); |
|
xpath.setNamespaceContext(nsc); |
|
return xpath; |
|
} |
|
|
|
private static String fetchXmlString(Connection conn) throws SQLException { |
|
Statement statement = conn.createStatement(); |
|
ResultSet resultSet = statement.executeQuery("select xmlfile from res_output"); |
|
resultSet.next(); |
|
String xmlString = resultSet.getString("xmlfile"); |
|
return xmlString; |
|
} |
|
|
|
private static void validateAgainstSchema(Document document) throws SAXException, IOException { |
|
SchemaFactory fac = new XMLSchemaFactory(); |
|
File schemaFile = new File(SCHEMA_LOCATION); |
|
Schema schema = fac.newSchema(schemaFile); |
|
Validator validator = schema.newValidator(); |
|
Source source = new DOMSource(document); |
|
validator.validate(source); |
|
} |
|
|
|
private static void writeToFile(Document document, int year) { |
|
try { |
|
Transformer transformer = TransformerFactory.newInstance().newTransformer(); |
|
transformer.setOutputProperty(OutputKeys.INDENT, "yes"); |
|
DOMSource source = new DOMSource(document); |
|
// insert your output directory here |
|
OutputStream out = new FileOutputStream(REFERENCE_FILE_LOCATION_BASE + year + ".xml"); |
|
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); |
|
StreamResult streamResult = new StreamResult(writer); |
|
transformer.transform(source, streamResult); |
|
writer.flush(); |
|
} catch (Exception e) { |
|
// nothing happening here... |
|
} |
|
} |
|
|
|
@AfterClass |
|
public static void after() { |
|
KettleEnvironment.shutdown(); |
|
} |
|
|
|
}
|
|
|