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 extends Parameter> 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: }