package de.superx.db.export; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; import org.springframework.stereotype.Component; /** * Reads export group configuration from an XML file. * * Expected format: *
{@code
 * 
 *   
 *     table_one
* table_two
*
*
* }
*/ @Component public class ExportConfigReader { public ExportConfig read(File configFile) throws JDOMException, IOException { SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(configFile); return parse(doc); } public ExportConfig read(InputStream in) throws JDOMException, IOException { SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); return parse(doc); } private ExportConfig parse(Document doc) { Element root = doc.getRootElement(); List groups = new ArrayList<>(); for (Element groupEl : root.getChildren("group")) { String name = groupEl.getAttributeValue("name"); if (name == null || name.isBlank()) { throw new IllegalArgumentException("Export group must have a 'name' attribute"); } String schema = groupEl.getAttributeValue("schema"); // Parse optional filter String filterColumn = null; String filterSource = null; Element filterEl = groupEl.getChild("filter"); if (filterEl != null) { filterColumn = filterEl.getAttributeValue("column"); if (filterColumn == null || filterColumn.isBlank()) { throw new IllegalArgumentException( "Filter in group '" + name + "' must have a 'column' attribute"); } String sourceAttr = filterEl.getAttributeValue("source"); String valuesAttr = filterEl.getAttributeValue("values"); if (sourceAttr != null && !sourceAttr.isBlank()) { filterSource = sourceAttr; } else if (valuesAttr != null && !valuesAttr.isBlank()) { filterSource = buildValuesSubquery(valuesAttr); } else { throw new IllegalArgumentException( "Filter in group '" + name + "' must have a 'source' or 'values' attribute"); } } // Parse tables with optional per-table column, source, columns, and orderBy overrides List tableConfigs = new ArrayList<>(); for (Element tableEl : groupEl.getChildren("table")) { String tableName = tableEl.getTextTrim(); if (!tableName.isEmpty()) { String colOverride = tableEl.getAttributeValue("column"); String sourceOverride = tableEl.getAttributeValue("source"); String columnsAttr = tableEl.getAttributeValue("columns"); String orderByAttr = tableEl.getAttributeValue("orderBy"); List columnsList = null; if (columnsAttr != null && !columnsAttr.isBlank()) { columnsList = new ArrayList<>(); for (String col : columnsAttr.split(",")) { String trimmed = col.trim(); if (!trimmed.isEmpty()) { columnsList.add(trimmed); } } } tableConfigs.add(new ExportTableConfig( tableName, colOverride, sourceOverride, columnsList, orderByAttr)); } } if (tableConfigs.isEmpty()) { throw new IllegalArgumentException("Export group '" + name + "' has no tables defined"); } groups.add(new ExportGroup(name, schema, tableConfigs, filterColumn, filterSource)); } return new ExportConfig(groups); } /** * Converts a comma-separated values string into a SQL VALUES clause. * Each value is trimmed. Numeric values are kept as-is; * non-numeric values are single-quoted. * * Example: "100, 200, 300" → "VALUES (100),(200),(300)" * Example: "ZUL, FIN" → "VALUES ('ZUL'),('FIN')" */ private String buildValuesSubquery(String valuesAttr) { String[] parts = valuesAttr.split(","); StringBuilder sb = new StringBuilder("VALUES "); for (int i = 0; i < parts.length; i++) { String val = parts[i].trim(); if (val.isEmpty()) { continue; } if (i > 0) { sb.append(","); } if (isNumeric(val)) { sb.append("(").append(val).append(")"); } else { sb.append("('").append(val.replace("'", "''")).append("')"); } } return sb.toString(); } private boolean isNumeric(String s) { try { Double.parseDouble(s); return true; } catch (NumberFormatException e) { return false; } } }