franta-hg@29: /** franta-hg@29: * SQL-DK franta-hg@29: * Copyright © 2013 František Kučera (frantovo.cz) franta-hg@29: * franta-hg@29: * This program is free software: you can redistribute it and/or modify franta-hg@29: * it under the terms of the GNU General Public License as published by franta-hg@29: * the Free Software Foundation, either version 3 of the License, or franta-hg@29: * (at your option) any later version. franta-hg@29: * franta-hg@29: * This program is distributed in the hope that it will be useful, franta-hg@29: * but WITHOUT ANY WARRANTY; without even the implied warranty of franta-hg@29: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the franta-hg@29: * GNU General Public License for more details. franta-hg@29: * franta-hg@29: * You should have received a copy of the GNU General Public License franta-hg@29: * along with this program. If not, see . franta-hg@29: */ franta-hg@29: package info.globalcode.sql.dk.formatting; franta-hg@29: franta-hg@128: import info.globalcode.sql.dk.Parameter; franta-hg@128: import info.globalcode.sql.dk.Xmlns; franta-hg@128: import info.globalcode.sql.dk.configuration.DatabaseDefinition; franta-hg@128: import static info.globalcode.sql.dk.Functions.notNull; franta-hg@128: import info.globalcode.sql.dk.NamedParameter; franta-hg@206: import info.globalcode.sql.dk.configuration.PropertyDeclaration; franta-hg@165: import java.sql.Array; franta-hg@233: import java.sql.ResultSet; franta-hg@165: import java.sql.SQLException; franta-hg@225: import java.sql.SQLXML; franta-hg@245: import java.sql.Types; franta-hg@233: import java.util.ArrayList; franta-hg@245: import java.util.Collections; franta-hg@245: import java.util.HashMap; franta-hg@128: import java.util.LinkedHashMap; franta-hg@128: import java.util.List; franta-hg@128: import java.util.Map; franta-hg@165: import java.util.logging.Level; franta-hg@165: import java.util.logging.Logger; franta-hg@128: import javax.xml.namespace.QName; franta-hg@128: franta-hg@29: /** franta-hg@206: *

franta-hg@206: * Prints machine-readable output – XML document containing resultsets and updates count. Good franta-hg@155: * choice for further processing – e.g. XSL transformation.

franta-hg@155: * franta-hg@206: *

franta-hg@245: * XML format is defined in the Relational franta-hg@245: * pipes specification. franta-hg@245: *

franta-hg@29: * franta-hg@29: * @author Ing. František Kučera (frantovo.cz) franta-hg@29: */ franta-hg@207: @PropertyDeclaration(name = XmlFormatter.PROPERTY_LABELED_COLUMNS, defaultValue = "false", type = Boolean.class, description = "whether to add 'label' attribute to each 'column' element") franta-hg@128: public class XmlFormatter extends AbstractXmlFormatter { franta-hg@29: franta-hg@245: private static final String XML_ELEMENT_RELATION = "relation"; franta-hg@245: private static final String XML_ELEMENT_NAME = "name"; franta-hg@245: private static final String XML_ELEMENT_RECORD = "record"; franta-hg@245: private static final String XML_ELEMENT_ATTRIBUTE = "attribute"; franta-hg@245: private static final String XML_ATTRIBUTE_NAME = "name"; franta-hg@245: private static final String XML_ATTRIBUTE_TYPE = "type"; franta-hg@245: private static final String XML_ATTRIBUTE_STATEMENT = "statement"; franta-hg@245: private static final String XML_NS_PREFIX_SQLDK = "sql-dk"; franta-hg@245: franta-hg@245: private static final String RELPIPE_TYPE_BOOLEAN = "boolean"; franta-hg@245: private static final String RELPIPE_TYPE_INTEGER = "integer"; franta-hg@245: private static final String RELPIPE_TYPE_STRING = "string"; franta-hg@245: private static final Map RELPIPE_TYPES; franta-hg@245: franta-hg@245: static { franta-hg@245: Map m = new HashMap<>(); franta-hg@245: m.put(Types.BOOLEAN, RELPIPE_TYPE_BOOLEAN); franta-hg@245: m.put(Types.BIT, RELPIPE_TYPE_BOOLEAN); // TODO: relpipe "boolean" can not be null in the current version franta-hg@245: // m.put(Types.INTEGER, RELPIPE_TYPE_INTEGER); // relpipe "integer" is unsigned franta-hg@245: // TODO: add more types when supported in Relational pipes franta-hg@245: m.put(Types.CHAR, RELPIPE_TYPE_STRING); franta-hg@245: m.put(Types.VARCHAR, RELPIPE_TYPE_STRING); franta-hg@245: RELPIPE_TYPES = Collections.unmodifiableMap(m); franta-hg@245: } franta-hg@245: franta-hg@79: public static final String NAME = "xml"; // bash-completion:formatter franta-hg@131: public static final String PROPERTY_LABELED_COLUMNS = "labeledColumns"; franta-hg@165: private static final Logger log = Logger.getLogger(XmlFormatter.class.getName()); franta-hg@131: private final boolean labeledColumns; franta-hg@29: franta-hg@245: private String currentDatabaseName; franta-hg@245: private int statementCounter; franta-hg@248: franta-hg@245: franta-hg@29: public XmlFormatter(FormatterContext formatterContext) { franta-hg@29: super(formatterContext); franta-hg@131: labeledColumns = formatterContext.getProperties().getBoolean(PROPERTY_LABELED_COLUMNS, false); franta-hg@29: } franta-hg@128: franta-hg@245: private QName qname(String localPart) { franta-hg@245: return new QName(Xmlns.RELPIPE, localPart); franta-hg@245: } franta-hg@245: franta-hg@245: private QName qnameDK(String localPart) { franta-hg@245: return new QName(Xmlns.SQLDK, localPart, XML_NS_PREFIX_SQLDK); franta-hg@245: } franta-hg@245: franta-hg@128: @Override franta-hg@128: public void writeStartBatch() { franta-hg@128: super.writeStartBatch(); franta-hg@128: printStartDocument(); franta-hg@245: Map attributes = new LinkedHashMap<>(2); franta-hg@245: attributes.put(qname("xmlns"), Xmlns.RELPIPE); franta-hg@245: attributes.put(new QName(null, XML_NS_PREFIX_SQLDK, "xmlns"), Xmlns.SQLDK); franta-hg@245: printStartElement(qname("relpipe"), attributes); franta-hg@245: statementCounter = 0; franta-hg@245: franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@128: public void writeEndBatch() { franta-hg@128: super.writeEndBatch(); franta-hg@128: printEndElement(); franta-hg@128: printEndDocument(); franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@128: public void writeStartDatabase(DatabaseDefinition databaseDefinition) { franta-hg@128: super.writeStartDatabase(databaseDefinition); franta-hg@245: currentDatabaseName = databaseDefinition.getName(); franta-hg@128: Map attributes = databaseDefinition.getName() == null ? null : singleAttribute(qname("name"), databaseDefinition.getName()); franta-hg@245: printEmptyElement(qnameDK("database-start"), attributes); franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@128: public void writeEndDatabase() { franta-hg@128: super.writeEndDatabase(); franta-hg@245: Map attributes = currentDatabaseName == null ? null : singleAttribute(qname("name"), currentDatabaseName); franta-hg@245: printEmptyElement(qnameDK("database-end"), attributes); franta-hg@245: } franta-hg@245: franta-hg@245: private String getCurrentStatementName() { franta-hg@245: return "s" + statementCounter; franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@142: public void writeStartStatement() { franta-hg@142: super.writeStartStatement(); franta-hg@245: statementCounter++; franta-hg@245: printEmptyElement(qnameDK("statement-start"), singleAttribute(qname("id"), getCurrentStatementName())); franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@142: public void writeEndStatement() { franta-hg@142: super.writeEndStatement(); franta-hg@245: printEmptyElement(qnameDK("statement-end"), singleAttribute(qname("id"), getCurrentStatementName())); franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@128: public void writeQuery(String sql) { franta-hg@128: super.writeQuery(sql); franta-hg@245: printTextElement(qnameDK("sql"), singleAttribute(qname(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()), sql); franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@128: public void writeParameters(List parameters) { franta-hg@128: super.writeParameters(parameters); franta-hg@128: franta-hg@245: if (parameters != null && parameters.size() > 0) { franta-hg@128: franta-hg@245: printStartElement(qnameDK("parameters"), singleAttribute(qname(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName())); franta-hg@245: franta-hg@245: for (Parameter p : notNull(parameters)) { franta-hg@245: franta-hg@245: Map attributes = new LinkedHashMap<>(2); franta-hg@245: if (p instanceof NamedParameter) { franta-hg@245: attributes.put(qname(XML_ATTRIBUTE_NAME), ((NamedParameter) p).getName()); franta-hg@245: } franta-hg@245: attributes.put(qname(XML_ATTRIBUTE_TYPE), p.getType().name()); franta-hg@245: franta-hg@245: printTextElement(qnameDK("parameter"), attributes, String.valueOf(p.getValue())); franta-hg@128: } franta-hg@128: franta-hg@245: printEndElement(); franta-hg@245: franta-hg@128: } franta-hg@128: franta-hg@128: } franta-hg@128: franta-hg@128: @Override franta-hg@142: public void writeStartResultSet(ColumnsHeader header) { franta-hg@142: super.writeStartResultSet(header); franta-hg@245: printStartElement(qname(XML_ELEMENT_RELATION), singleAttribute(qnameDK(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName())); franta-hg@245: printTextElement(qname(XML_ELEMENT_NAME), null, getCurrentRelationName()); franta-hg@128: franta-hg@245: printStartElement(qname("attributes-metadata")); franta-hg@128: for (ColumnDescriptor cd : header.getColumnDescriptors()) { franta-hg@245: Map attributes = new LinkedHashMap<>(6); franta-hg@245: franta-hg@245: attributes.put(qname(XML_ATTRIBUTE_NAME), cd.getLabel()); franta-hg@245: attributes.put(qname(XML_ATTRIBUTE_TYPE), toRelpipeType(cd.getType())); franta-hg@245: franta-hg@245: attributes.put(qnameDK("tableName"), cd.getTableName()); franta-hg@245: attributes.put(qnameDK("columnName"), cd.getName()); franta-hg@245: attributes.put(qnameDK("jdbcTypeName"), cd.getTypeName()); franta-hg@245: attributes.put(qnameDK("jdbcType"), String.valueOf(cd.getType())); franta-hg@245: franta-hg@245: printEmptyElement(qname("attribute-metadata"), attributes); franta-hg@128: } franta-hg@245: printEndElement(); franta-hg@245: } franta-hg@245: franta-hg@245: /** franta-hg@245: * @param jdbcType value from {@linkplain Types} franta-hg@245: * @return franta-hg@245: */ franta-hg@245: private String toRelpipeType(int jdbcType) { franta-hg@245: return RELPIPE_TYPES.getOrDefault(jdbcType, RELPIPE_TYPE_STRING); franta-hg@128: } franta-hg@129: franta-hg@129: @Override franta-hg@142: public void writeEndResultSet() { franta-hg@142: super.writeEndResultSet(); franta-hg@142: printEndElement(); franta-hg@142: } franta-hg@142: franta-hg@142: @Override franta-hg@129: public void writeStartRow() { franta-hg@129: super.writeStartRow(); franta-hg@245: printStartElement(qname(XML_ELEMENT_RECORD)); franta-hg@129: } franta-hg@129: franta-hg@129: @Override franta-hg@129: public void writeColumnValue(Object value) { franta-hg@129: super.writeColumnValue(value); franta-hg@131: franta-hg@131: Map attributes = null; franta-hg@131: if (labeledColumns) { franta-hg@151: attributes = new LinkedHashMap<>(2); franta-hg@245: attributes.put(qname(XML_ATTRIBUTE_NAME), getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getLabel()); franta-hg@245: attributes.put(qname(XML_ATTRIBUTE_TYPE), toRelpipeType(getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getType())); franta-hg@131: } franta-hg@131: franta-hg@151: if (value == null) { franta-hg@151: if (attributes == null) { franta-hg@151: attributes = new LinkedHashMap<>(2); franta-hg@151: } franta-hg@245: // TODO: synchronize syntax with Relational pipes (after adding support of null values) franta-hg@245: attributes.put(qnameDK("null"), "true"); franta-hg@245: printEmptyElement(qname(XML_ELEMENT_ATTRIBUTE), attributes); franta-hg@165: } else if (value instanceof Array) { franta-hg@206: franta-hg@165: Array sqlArray = (Array) value; franta-hg@165: try { franta-hg@165: Object[] array = (Object[]) sqlArray.getArray(); franta-hg@245: printStartElement(qname(XML_ELEMENT_ATTRIBUTE), attributes); franta-hg@165: printArray(array); franta-hg@165: printEndElement(); franta-hg@165: } catch (SQLException e) { franta-hg@233: // FIXME: rewrite array formatting, remember array mode, don't try sqlArray.getArray() again and again if it has failed franta-hg@165: log.log(Level.SEVERE, "Unable to format array", e); franta-hg@233: try { franta-hg@233: ResultSet arrayResultSet = sqlArray.getResultSet(); franta-hg@233: //int columnCount = arrayResultSet.getMetaData().getColumnCount(); franta-hg@233: ArrayList arrayList = new ArrayList<>(); franta-hg@233: while (arrayResultSet.next()) { franta-hg@233: arrayList.add(arrayResultSet.getObject(2)); franta-hg@233: // for (int i = 1; i <= columnCount; i++) { franta-hg@233: // log.log(Level.INFO, "Array column {0} = {1}", new Object[]{i, arrayResultSet.getObject(i)}); franta-hg@233: // } franta-hg@233: } franta-hg@233: franta-hg@245: printStartElement(qname(XML_ELEMENT_ATTRIBUTE), attributes); franta-hg@233: // FIXME: instanceof SQLXML, see below franta-hg@233: printArray(arrayList.toArray()); franta-hg@233: printEndElement(); franta-hg@233: franta-hg@233: } catch (SQLException e2) { franta-hg@233: // FIXME: fix logging, error recovery franta-hg@233: log.log(Level.SEVERE, "Second level fuck up !!!", e2); franta-hg@233: } franta-hg@233: franta-hg@165: writeColumnValue(String.valueOf(value)); franta-hg@165: } franta-hg@206: franta-hg@233: } else if (value instanceof SQLXML) { // FIXME: move to separate method, to AbstractFormatter? franta-hg@225: SQLXML xml = (SQLXML) value; franta-hg@225: // TODO: parse DOM/SAX and transplant XML, don't escape (optional) franta-hg@225: try { franta-hg@245: printTextElement(qname(XML_ELEMENT_ATTRIBUTE), attributes, xml.getString()); franta-hg@225: } catch (SQLException e) { franta-hg@225: log.log(Level.SEVERE, "Unable to format XML", e); franta-hg@225: writeColumnValue(String.valueOf(value)); franta-hg@225: } franta-hg@151: } else { franta-hg@245: printTextElement(qname(XML_ELEMENT_ATTRIBUTE), attributes, toString(value)); franta-hg@151: } franta-hg@129: } franta-hg@206: franta-hg@165: private void printArray(Object[] array) { franta-hg@245: // TODO: synchronize array syntax with Relational pipes franta-hg@245: printStartElement(qnameDK("array")); franta-hg@165: for (Object o : array) { franta-hg@165: if (o instanceof Object[]) { franta-hg@245: printStartElement(qnameDK("item")); franta-hg@165: printArray((Object[]) o); franta-hg@165: printEndElement(); franta-hg@165: } else { franta-hg@245: printTextElement(qnameDK("item"), null, String.valueOf(o)); franta-hg@165: } franta-hg@165: } franta-hg@165: printEndElement(); franta-hg@165: } franta-hg@129: franta-hg@129: @Override franta-hg@129: public void writeEndRow() { franta-hg@129: super.writeEndRow(); franta-hg@129: printEndElement(); franta-hg@129: } franta-hg@129: franta-hg@129: @Override franta-hg@142: public void writeUpdatesResult(int updatedRowsCount) { franta-hg@142: super.writeUpdatesResult(updatedRowsCount); franta-hg@245: printTextElement(qnameDK("updatedRecords"), singleAttribute(qnameDK(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()), String.valueOf(updatedRowsCount)); franta-hg@129: } franta-hg@129: franta-hg@129: protected String toString(Object value) { franta-hg@129: return String.valueOf(value); franta-hg@129: } franta-hg@29: }