java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/XmlFormatter.java
branchv_0
changeset 245 b6ff5b7a8422
parent 238 4a1864c3e867
child 246 277c18b48762
     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) {