/* * Copyright 2007 - 2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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 net.sf.jailer.xml; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerConfigurationException; import org.apache.log4j.Logger; import org.w3c.dom.Document; import org.xml.sax.SAXException; import net.sf.jailer.ExecutionContext; import net.sf.jailer.configuration.DBMS; import net.sf.jailer.database.Session; import net.sf.jailer.database.Session.AbstractResultSetReader; import net.sf.jailer.database.Session.ResultSetReader; import net.sf.jailer.datamodel.AggregationSchema; import net.sf.jailer.datamodel.Association; import net.sf.jailer.datamodel.Column; import net.sf.jailer.datamodel.RowIdSupport; import net.sf.jailer.datamodel.Table; import net.sf.jailer.entitygraph.EntityGraph; import net.sf.jailer.util.CellContentConverter; import net.sf.jailer.util.Quoting; /** * A {@link ResultSetReader} that writes the read rows into an XML file. * * @author Ralf Wisser */ public class XmlExportTransformer extends AbstractResultSetReader { /** * The table to read from. */ private Table table; /** * For writing rows as xml. */ private final XmlRowWriter xmlRowWriter; /** * Counts the exported entities. (GUI support) */ public static long numberOfExportedEntities; /** * Counts the exported LOBs. (GUI support) */ public static long numberOfExportedLOBs; /** * The entity graph. */ private final EntityGraph entityGraph; /** * Set of all tables for which entities exist in entityGraph. */ private final Set<Table> totalProgress; /** * Set of all cyclic aggregated tables. */ private final Set<Table> cyclicAggregatedTables; /** * The logger. */ private static final Logger _log = Logger.getLogger(XmlExportTransformer.class); /** * Maps clear text SQL-types to {@link java.sql.Types}. */ private Map<Table, Map<String, Integer>> typeCache = new HashMap<Table, Map<String,Integer>>(); /** * Current session; */ private final Session session; /** * Association cache. */ private Map<Table, Map<String, Association>> associationCache = new HashMap<Table, Map<String,Association>>(); /** * {@link RowIdSupport}. */ private final RowIdSupport rowIdSupport; private final Quoting quoting; /** * Constructor. * * @param out to write the xml into * @param commentHeader comment at top of document * @param entityGraph the entity graph * @param totalProgress set of all tables for which entities exist in entityGraph * @param rootTag root tag name * @param datePattern pattern for dates * @param timestampPattern pattern for time-stamps * @throws SQLException */ public XmlExportTransformer(OutputStream out, String commentHeader, EntityGraph entityGraph, Set<Table> totalProgress, Set<Table> cyclicAggregatedTables, String rootTag, String datePattern, String timestampPattern, Session session, Charset charset, ExecutionContext executionContext) throws TransformerConfigurationException, SAXException, SQLException { this.xmlRowWriter = new XmlRowWriter(out, commentHeader, rootTag, datePattern, timestampPattern, charset); this.entityGraph = entityGraph; this.totalProgress = totalProgress; this.cyclicAggregatedTables = cyclicAggregatedTables; this.session = session; this.quoting = new Quoting(session); this.rowIdSupport = new RowIdSupport(entityGraph.getDatamodel(), session.dbms, executionContext); } /** * Reads result-set and writes into export-script. */ public void readCurrentRow(ResultSet resultSet) throws SQLException { try { writeEntity(table, null, resultSet, new ArrayList<String>(), getCellContentConverter(resultSet, session, session.dbms)); } catch (SAXException e) { throw new RuntimeException(e); } catch (ParserConfigurationException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } /** * Writes entity as XML hierarchy. * * @param table entity's table * @param association association to parent, <code>null</code> for top-level entities * @param resultSet current row contains entity to write out * @param ancestors ancestors of entity to write out */ private void writeEntity(final Table table, Association association, final ResultSet resultSet, final List<String> ancestors, final CellContentConverter cellContentConverter) throws SQLException, SAXException, ParserConfigurationException, IOException { StringBuilder sb = new StringBuilder(table.getName() + "("); boolean f = true; int i = 0; for (@SuppressWarnings("unused") Column pk : rowIdSupport.getPrimaryKey(table).getColumns()) { if (!f) { sb.append(", "); } f = false; sb.append(cellContentConverter.toSql(cellContentConverter.getObject(resultSet, "PK" + i++))); } sb.append(")"); String primaryKey = sb.toString(); if (ancestors.contains(primaryKey)) { throw new RuntimeException("cyclic aggregation: " + primaryKey + " aggregates itself"); } ancestors.add(primaryKey); TableMapping tableMapping = getTableMapping(table); Map<String, Association> associationMap = associationCache.get(table); if (associationMap == null) { associationMap = new HashMap<String, Association>(); for (Association a: table.associations) { associationMap.put(a.getName(), a); } associationCache.put(table, associationMap); } final Map<String, Association> finalAssociationMap = associationMap; XmlUtil.visitDocumentNodes(tableMapping.template, xmlRowWriter.new XmlWritingNodeVisitor(resultSet, getMetaData(resultSet), table, association, session) { public void visitAssociationElement(String associationName) { final Association sa = finalAssociationMap.get(associationName); if (sa != null) { if (totalProgress.contains(sa.destination)) { if (sa.getAggregationSchema() != AggregationSchema.NONE) { ResultSetReader reader = new ResultSetReader() { public void readCurrentRow(ResultSet resultSet) throws SQLException { try { writeEntity(sa.destination, sa, resultSet, ancestors, getCellContentConverter(resultSet, session, session.dbms)); } catch (SAXException e) { throw new RuntimeException(e); } catch (ParserConfigurationException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } public void close() { } }; try { xmlRowWriter.startList(sa); entityGraph.readDependentEntities(sa.destination, sa, resultSet, getMetaData(resultSet), reader, getTypeCache(sa.destination), getTableMapping(sa.destination).selectionSchema, getTableMapping(sa.destination).originalPKAliasPrefix); if (cyclicAggregatedTables.contains(sa.destination)) { entityGraph.markDependentEntitiesAsTraversed(sa, resultSet, getMetaData(resultSet), getTypeCache(sa.destination)); } xmlRowWriter.endList(sa); } catch (Exception e) { throw new RuntimeException(e); } } } } } }); ancestors.remove(ancestors.size() - 1); } /** * Gets type cache for given table. * * @param table the table * @return type cache for table */ private Map<String, Integer> getTypeCache(Table table) { Map<String, Integer> cache = typeCache.get(table); if (cache == null) { cache = new HashMap<String, Integer>(); typeCache.put(table, cache); } return cache; } /** * Sets the table to read from. */ public void setTable(Table table) { this.table = table; } /** * Closes the XML document. */ public void endDocument() throws SAXException { xmlRowWriter.close(); } /** * Flushes the export-reader. */ public void close() { } /** * Holds XML mapping information. */ public class TableMapping { /** * The template. */ public Document template; /** * SQL selection schema. */ public String selectionSchema; /** * Prefix of column aliases for selection of unfiltered PK values. */ public String originalPKAliasPrefix; } /** * Mappings per table. */ private Map<Table, TableMapping> tableMappings = new HashMap<Table, TableMapping>(); /** * Gets the xml mapping for a table. * * @param table the table * @return xml mapping for table */ public TableMapping getTableMapping(Table table) throws SAXException, IOException { if (tableMappings.containsKey(table)) { return tableMappings.get(table); } TableMapping tableMapping = new TableMapping(); tableMappings.put(table, tableMapping); boolean isFiltered = false; for (Column c: table.getColumns()) { String filterExpression = null; if (c.getFilter() != null && c.getFilter().isApplyAtExport()) { filterExpression = c.getFilterExpression(); } if (filterExpression != null) { isFiltered = true; break; } } if (isFiltered) { for (int i = 0; ; ++i) { tableMapping.originalPKAliasPrefix = "O" + i; boolean found = false; for (Column c: table.getColumns()) { if (c.name.startsWith(tableMapping.originalPKAliasPrefix)) { found = true; break; } } if (!found) { break; } } } try { tableMapping.template = table.getXmlTemplateAsDocument(quoting); } catch (Exception e) { // try again with default template, // there was a bug in Jailer 3.0 which causes corruption of XML templates // in windows platform _log.warn("can't parse XML template for table " + table.getName() + ", using defaults", e); try { tableMapping.template = table.getDefaultXmlTemplate(quoting); } catch (ParserConfigurationException e1) { throw new RuntimeException(e1); } } final StringBuilder sb = new StringBuilder(); int i = 0; for (Column pk: rowIdSupport.getPrimaryKey(table).getColumns()) { if (sb.length() > 0) { sb.append(", "); } if (tableMapping.originalPKAliasPrefix != null) { sb.append("T." + tableMapping.originalPKAliasPrefix + i + " AS PK" + i); ++i; } else { sb.append("T." + quoting.requote(pk.name) + " AS PK" + i++); } } XmlUtil.visitDocumentNodes(tableMapping.template, new NodeVisitor() { int nr = 0; private void appendSchema(String text) { if (text != null && text.startsWith(XmlUtil.SQL_PREFIX)) { if (sb.length() > 0) { sb.append(", "); } sb.append(text.substring(XmlUtil.SQL_PREFIX.length()) + " AS C" + nr++); } } public void visitAssociationElement(String associationName) { } public void visitComment(String comment) { } public void visitElementEnd(String elementName, boolean isRoot) { } public void visitText(String text) { appendSchema(text); } public void visitElementStart(String elementName, boolean isRoot, String[] attributeNames, String[] attributeValues) { for (String value: attributeValues) { appendSchema(value); } } }); tableMapping.selectionSchema = sb.toString(); return tableMapping; } }