1.1 --- a/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/XmlFormatter.java Mon Mar 04 22:28:29 2019 +0100
1.2 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/XmlFormatter.java Tue Mar 05 21:22:33 2019 +0100
1.3 @@ -23,12 +23,14 @@
1.4 import static info.globalcode.sql.dk.Functions.notNull;
1.5 import info.globalcode.sql.dk.NamedParameter;
1.6 import info.globalcode.sql.dk.configuration.PropertyDeclaration;
1.7 -import static info.globalcode.sql.dk.formatting.AbstractXmlFormatter.qname;
1.8 import java.sql.Array;
1.9 import java.sql.ResultSet;
1.10 import java.sql.SQLException;
1.11 import java.sql.SQLXML;
1.12 +import java.sql.Types;
1.13 import java.util.ArrayList;
1.14 +import java.util.Collections;
1.15 +import java.util.HashMap;
1.16 import java.util.LinkedHashMap;
1.17 import java.util.List;
1.18 import java.util.Map;
1.19 @@ -42,28 +44,73 @@
1.20 * choice for further processing – e.g. XSL transformation.</p>
1.21 *
1.22 * <p>
1.23 - * TODO: XSD</p>
1.24 + * XML format is defined in the <a href="https://relational-pipes.globalcode.info/">Relational
1.25 + * pipes</a> specification.
1.26 + * </p>
1.27 *
1.28 * @author Ing. František Kučera (frantovo.cz)
1.29 */
1.30 @PropertyDeclaration(name = XmlFormatter.PROPERTY_LABELED_COLUMNS, defaultValue = "false", type = Boolean.class, description = "whether to add 'label' attribute to each 'column' element")
1.31 public class XmlFormatter extends AbstractXmlFormatter {
1.32
1.33 + private static final String XML_ELEMENT_RELATION = "relation";
1.34 + private static final String XML_ELEMENT_NAME = "name";
1.35 + private static final String XML_ELEMENT_RECORD = "record";
1.36 + private static final String XML_ELEMENT_ATTRIBUTE = "attribute";
1.37 + private static final String XML_ATTRIBUTE_NAME = "name";
1.38 + private static final String XML_ATTRIBUTE_TYPE = "type";
1.39 + private static final String XML_ATTRIBUTE_STATEMENT = "statement";
1.40 + private static final String XML_NS_PREFIX_SQLDK = "sql-dk";
1.41 +
1.42 + private static final String RELPIPE_TYPE_BOOLEAN = "boolean";
1.43 + private static final String RELPIPE_TYPE_INTEGER = "integer";
1.44 + private static final String RELPIPE_TYPE_STRING = "string";
1.45 + private static final Map<Integer, String> RELPIPE_TYPES;
1.46 +
1.47 + static {
1.48 + Map<Integer, String> m = new HashMap<>();
1.49 + m.put(Types.BOOLEAN, RELPIPE_TYPE_BOOLEAN);
1.50 + m.put(Types.BIT, RELPIPE_TYPE_BOOLEAN); // TODO: relpipe "boolean" can not be null in the current version
1.51 + // m.put(Types.INTEGER, RELPIPE_TYPE_INTEGER); // relpipe "integer" is unsigned
1.52 + // TODO: add more types when supported in Relational pipes
1.53 + m.put(Types.CHAR, RELPIPE_TYPE_STRING);
1.54 + m.put(Types.VARCHAR, RELPIPE_TYPE_STRING);
1.55 + RELPIPE_TYPES = Collections.unmodifiableMap(m);
1.56 + }
1.57 +
1.58 public static final String NAME = "xml"; // bash-completion:formatter
1.59 public static final String PROPERTY_LABELED_COLUMNS = "labeledColumns";
1.60 private static final Logger log = Logger.getLogger(XmlFormatter.class.getName());
1.61 private final boolean labeledColumns;
1.62
1.63 + private String currentDatabaseName;
1.64 + private int statementCounter;
1.65 + private int resultSetCounter;
1.66 +
1.67 public XmlFormatter(FormatterContext formatterContext) {
1.68 super(formatterContext);
1.69 labeledColumns = formatterContext.getProperties().getBoolean(PROPERTY_LABELED_COLUMNS, false);
1.70 }
1.71
1.72 + private QName qname(String localPart) {
1.73 + return new QName(Xmlns.RELPIPE, localPart);
1.74 + }
1.75 +
1.76 + private QName qnameDK(String localPart) {
1.77 + return new QName(Xmlns.SQLDK, localPart, XML_NS_PREFIX_SQLDK);
1.78 + }
1.79 +
1.80 @Override
1.81 public void writeStartBatch() {
1.82 super.writeStartBatch();
1.83 printStartDocument();
1.84 - printStartElement(qname("batchResult"), singleAttribute(qname("xmlns"), Xmlns.BATCH_RESULT));
1.85 + Map<QName, String> attributes = new LinkedHashMap<>(2);
1.86 + attributes.put(qname("xmlns"), Xmlns.RELPIPE);
1.87 + attributes.put(new QName(null, XML_NS_PREFIX_SQLDK, "xmlns"), Xmlns.SQLDK);
1.88 + printStartElement(qname("relpipe"), attributes);
1.89 + statementCounter = 0;
1.90 + resultSetCounter = 0;
1.91 +
1.92 }
1.93
1.94 @Override
1.95 @@ -76,64 +123,101 @@
1.96 @Override
1.97 public void writeStartDatabase(DatabaseDefinition databaseDefinition) {
1.98 super.writeStartDatabase(databaseDefinition);
1.99 + currentDatabaseName = databaseDefinition.getName();
1.100 Map<QName, String> attributes = databaseDefinition.getName() == null ? null : singleAttribute(qname("name"), databaseDefinition.getName());
1.101 - printStartElement(qname("database"), attributes);
1.102 + printEmptyElement(qnameDK("database-start"), attributes);
1.103 }
1.104
1.105 @Override
1.106 public void writeEndDatabase() {
1.107 super.writeEndDatabase();
1.108 - printEndElement();
1.109 + Map<QName, String> attributes = currentDatabaseName == null ? null : singleAttribute(qname("name"), currentDatabaseName);
1.110 + printEmptyElement(qnameDK("database-end"), attributes);
1.111 + }
1.112 +
1.113 + private String getCurrentStatementName() {
1.114 + return "s" + statementCounter;
1.115 }
1.116
1.117 @Override
1.118 public void writeStartStatement() {
1.119 super.writeStartStatement();
1.120 - printStartElement(qname("statement"));
1.121 + statementCounter++;
1.122 + printEmptyElement(qnameDK("statement-start"), singleAttribute(qname("id"), getCurrentStatementName()));
1.123 }
1.124
1.125 @Override
1.126 public void writeEndStatement() {
1.127 super.writeEndStatement();
1.128 - printEndElement();
1.129 + printEmptyElement(qnameDK("statement-end"), singleAttribute(qname("id"), getCurrentStatementName()));
1.130 }
1.131
1.132 @Override
1.133 public void writeQuery(String sql) {
1.134 super.writeQuery(sql);
1.135 - printTextElement(qname("sql"), null, sql);
1.136 + printTextElement(qnameDK("sql"), singleAttribute(qname(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()), sql);
1.137 }
1.138
1.139 @Override
1.140 public void writeParameters(List<? extends Parameter> parameters) {
1.141 super.writeParameters(parameters);
1.142
1.143 - for (Parameter p : notNull(parameters)) {
1.144 + if (parameters != null && parameters.size() > 0) {
1.145
1.146 - Map<QName, String> attributes = new LinkedHashMap<>(2);
1.147 - if (p instanceof NamedParameter) {
1.148 - attributes.put(qname("name"), ((NamedParameter) p).getName());
1.149 + printStartElement(qnameDK("parameters"), singleAttribute(qname(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()));
1.150 +
1.151 + for (Parameter p : notNull(parameters)) {
1.152 +
1.153 + Map<QName, String> attributes = new LinkedHashMap<>(2);
1.154 + if (p instanceof NamedParameter) {
1.155 + attributes.put(qname(XML_ATTRIBUTE_NAME), ((NamedParameter) p).getName());
1.156 + }
1.157 + attributes.put(qname(XML_ATTRIBUTE_TYPE), p.getType().name());
1.158 +
1.159 + printTextElement(qnameDK("parameter"), attributes, String.valueOf(p.getValue()));
1.160 }
1.161 - attributes.put(qname("type"), p.getType().name());
1.162
1.163 - printTextElement(qname("parameter"), attributes, String.valueOf(p.getValue()));
1.164 + printEndElement();
1.165 +
1.166 }
1.167
1.168 }
1.169
1.170 + private String getCurrentRelationName() {
1.171 + // TODO: support custom names (add CLI option)
1.172 + return "r" + resultSetCounter;
1.173 + }
1.174 +
1.175 @Override
1.176 public void writeStartResultSet(ColumnsHeader header) {
1.177 super.writeStartResultSet(header);
1.178 - printStartElement(qname("resultSet"));
1.179 + resultSetCounter++;
1.180 + printStartElement(qname(XML_ELEMENT_RELATION), singleAttribute(qnameDK(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()));
1.181 + printTextElement(qname(XML_ELEMENT_NAME), null, getCurrentRelationName());
1.182
1.183 + printStartElement(qname("attributes-metadata"));
1.184 for (ColumnDescriptor cd : header.getColumnDescriptors()) {
1.185 - Map<QName, String> attributes = new LinkedHashMap<>(4);
1.186 - attributes.put(qname("label"), cd.getLabel());
1.187 - attributes.put(qname("name"), cd.getName());
1.188 - attributes.put(qname("typeName"), cd.getTypeName());
1.189 - attributes.put(qname("type"), String.valueOf(cd.getType()));
1.190 - printEmptyElement(qname("columnHeader"), attributes);
1.191 + Map<QName, String> attributes = new LinkedHashMap<>(6);
1.192 +
1.193 + attributes.put(qname(XML_ATTRIBUTE_NAME), cd.getLabel());
1.194 + attributes.put(qname(XML_ATTRIBUTE_TYPE), toRelpipeType(cd.getType()));
1.195 +
1.196 + attributes.put(qnameDK("tableName"), cd.getTableName());
1.197 + attributes.put(qnameDK("columnName"), cd.getName());
1.198 + attributes.put(qnameDK("jdbcTypeName"), cd.getTypeName());
1.199 + attributes.put(qnameDK("jdbcType"), String.valueOf(cd.getType()));
1.200 +
1.201 + printEmptyElement(qname("attribute-metadata"), attributes);
1.202 }
1.203 + printEndElement();
1.204 + }
1.205 +
1.206 + /**
1.207 + * @param jdbcType value from {@linkplain Types}
1.208 + * @return
1.209 + */
1.210 + private String toRelpipeType(int jdbcType) {
1.211 + return RELPIPE_TYPES.getOrDefault(jdbcType, RELPIPE_TYPE_STRING);
1.212 }
1.213
1.214 @Override
1.215 @@ -145,7 +229,7 @@
1.216 @Override
1.217 public void writeStartRow() {
1.218 super.writeStartRow();
1.219 - printStartElement(qname("row"));
1.220 + printStartElement(qname(XML_ELEMENT_RECORD));
1.221 }
1.222
1.223 @Override
1.224 @@ -155,21 +239,23 @@
1.225 Map<QName, String> attributes = null;
1.226 if (labeledColumns) {
1.227 attributes = new LinkedHashMap<>(2);
1.228 - attributes.put(qname("label"), getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getLabel());
1.229 + attributes.put(qname(XML_ATTRIBUTE_NAME), getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getLabel());
1.230 + attributes.put(qname(XML_ATTRIBUTE_TYPE), toRelpipeType(getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getType()));
1.231 }
1.232
1.233 if (value == null) {
1.234 if (attributes == null) {
1.235 attributes = new LinkedHashMap<>(2);
1.236 }
1.237 - attributes.put(qname("null"), "true");
1.238 - printEmptyElement(qname("column"), attributes);
1.239 + // TODO: synchronize syntax with Relational pipes (after adding support of null values)
1.240 + attributes.put(qnameDK("null"), "true");
1.241 + printEmptyElement(qname(XML_ELEMENT_ATTRIBUTE), attributes);
1.242 } else if (value instanceof Array) {
1.243
1.244 Array sqlArray = (Array) value;
1.245 try {
1.246 Object[] array = (Object[]) sqlArray.getArray();
1.247 - printStartElement(qname("column"), attributes);
1.248 + printStartElement(qname(XML_ELEMENT_ATTRIBUTE), attributes);
1.249 printArray(array);
1.250 printEndElement();
1.251 } catch (SQLException e) {
1.252 @@ -186,7 +272,7 @@
1.253 // }
1.254 }
1.255
1.256 - printStartElement(qname("column"), attributes);
1.257 + printStartElement(qname(XML_ELEMENT_ATTRIBUTE), attributes);
1.258 // FIXME: instanceof SQLXML, see below
1.259 printArray(arrayList.toArray());
1.260 printEndElement();
1.261 @@ -203,25 +289,26 @@
1.262 SQLXML xml = (SQLXML) value;
1.263 // TODO: parse DOM/SAX and transplant XML, don't escape (optional)
1.264 try {
1.265 - printTextElement(qname("column"), attributes, xml.getString());
1.266 + printTextElement(qname(XML_ELEMENT_ATTRIBUTE), attributes, xml.getString());
1.267 } catch (SQLException e) {
1.268 log.log(Level.SEVERE, "Unable to format XML", e);
1.269 writeColumnValue(String.valueOf(value));
1.270 }
1.271 } else {
1.272 - printTextElement(qname("column"), attributes, toString(value));
1.273 + printTextElement(qname(XML_ELEMENT_ATTRIBUTE), attributes, toString(value));
1.274 }
1.275 }
1.276
1.277 private void printArray(Object[] array) {
1.278 - printStartElement(qname("array"));
1.279 + // TODO: synchronize array syntax with Relational pipes
1.280 + printStartElement(qnameDK("array"));
1.281 for (Object o : array) {
1.282 if (o instanceof Object[]) {
1.283 - printStartElement(qname("item"));
1.284 + printStartElement(qnameDK("item"));
1.285 printArray((Object[]) o);
1.286 printEndElement();
1.287 } else {
1.288 - printTextElement(qname("item"), null, String.valueOf(o));
1.289 + printTextElement(qnameDK("item"), null, String.valueOf(o));
1.290 }
1.291 }
1.292 printEndElement();
1.293 @@ -236,7 +323,7 @@
1.294 @Override
1.295 public void writeUpdatesResult(int updatedRowsCount) {
1.296 super.writeUpdatesResult(updatedRowsCount);
1.297 - printTextElement(qname("updatedRows"), null, String.valueOf(updatedRowsCount));
1.298 + printTextElement(qnameDK("updatedRecords"), singleAttribute(qnameDK(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()), String.valueOf(updatedRowsCount));
1.299 }
1.300
1.301 protected String toString(Object value) {