java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/XmlFormatter.java
branchv_0
changeset 245 b6ff5b7a8422
parent 238 4a1864c3e867
child 246 277c18b48762
equal deleted inserted replaced
244:6bdb45af26d9 245:b6ff5b7a8422
    21 import info.globalcode.sql.dk.Xmlns;
    21 import info.globalcode.sql.dk.Xmlns;
    22 import info.globalcode.sql.dk.configuration.DatabaseDefinition;
    22 import info.globalcode.sql.dk.configuration.DatabaseDefinition;
    23 import static info.globalcode.sql.dk.Functions.notNull;
    23 import static info.globalcode.sql.dk.Functions.notNull;
    24 import info.globalcode.sql.dk.NamedParameter;
    24 import info.globalcode.sql.dk.NamedParameter;
    25 import info.globalcode.sql.dk.configuration.PropertyDeclaration;
    25 import info.globalcode.sql.dk.configuration.PropertyDeclaration;
    26 import static info.globalcode.sql.dk.formatting.AbstractXmlFormatter.qname;
       
    27 import java.sql.Array;
    26 import java.sql.Array;
    28 import java.sql.ResultSet;
    27 import java.sql.ResultSet;
    29 import java.sql.SQLException;
    28 import java.sql.SQLException;
    30 import java.sql.SQLXML;
    29 import java.sql.SQLXML;
       
    30 import java.sql.Types;
    31 import java.util.ArrayList;
    31 import java.util.ArrayList;
       
    32 import java.util.Collections;
       
    33 import java.util.HashMap;
    32 import java.util.LinkedHashMap;
    34 import java.util.LinkedHashMap;
    33 import java.util.List;
    35 import java.util.List;
    34 import java.util.Map;
    36 import java.util.Map;
    35 import java.util.logging.Level;
    37 import java.util.logging.Level;
    36 import java.util.logging.Logger;
    38 import java.util.logging.Logger;
    40  * <p>
    42  * <p>
    41  * Prints machine-readable output – XML document containing resultsets and updates count. Good
    43  * Prints machine-readable output – XML document containing resultsets and updates count. Good
    42  * choice for further processing – e.g. XSL transformation.</p>
    44  * choice for further processing – e.g. XSL transformation.</p>
    43  *
    45  *
    44  * <p>
    46  * <p>
    45  * TODO: XSD</p>
    47  * XML format is defined in the <a href="https://relational-pipes.globalcode.info/">Relational
       
    48  * pipes</a> specification.
       
    49  * </p>
    46  *
    50  *
    47  * @author Ing. František Kučera (frantovo.cz)
    51  * @author Ing. František Kučera (frantovo.cz)
    48  */
    52  */
    49 @PropertyDeclaration(name = XmlFormatter.PROPERTY_LABELED_COLUMNS, defaultValue = "false", type = Boolean.class, description = "whether to add 'label' attribute to each 'column' element")
    53 @PropertyDeclaration(name = XmlFormatter.PROPERTY_LABELED_COLUMNS, defaultValue = "false", type = Boolean.class, description = "whether to add 'label' attribute to each 'column' element")
    50 public class XmlFormatter extends AbstractXmlFormatter {
    54 public class XmlFormatter extends AbstractXmlFormatter {
    51 
    55 
       
    56 	private static final String XML_ELEMENT_RELATION = "relation";
       
    57 	private static final String XML_ELEMENT_NAME = "name";
       
    58 	private static final String XML_ELEMENT_RECORD = "record";
       
    59 	private static final String XML_ELEMENT_ATTRIBUTE = "attribute";
       
    60 	private static final String XML_ATTRIBUTE_NAME = "name";
       
    61 	private static final String XML_ATTRIBUTE_TYPE = "type";
       
    62 	private static final String XML_ATTRIBUTE_STATEMENT = "statement";
       
    63 	private static final String XML_NS_PREFIX_SQLDK = "sql-dk";
       
    64 
       
    65 	private static final String RELPIPE_TYPE_BOOLEAN = "boolean";
       
    66 	private static final String RELPIPE_TYPE_INTEGER = "integer";
       
    67 	private static final String RELPIPE_TYPE_STRING = "string";
       
    68 	private static final Map<Integer, String> RELPIPE_TYPES;
       
    69 
       
    70 	static {
       
    71 		Map<Integer, String> m = new HashMap<>();
       
    72 		m.put(Types.BOOLEAN, RELPIPE_TYPE_BOOLEAN);
       
    73 		m.put(Types.BIT, RELPIPE_TYPE_BOOLEAN); // TODO: relpipe "boolean" can not be null in the current version
       
    74 		// m.put(Types.INTEGER, RELPIPE_TYPE_INTEGER); // relpipe "integer" is unsigned
       
    75 		// TODO: add more types when supported in Relational pipes
       
    76 		m.put(Types.CHAR, RELPIPE_TYPE_STRING);
       
    77 		m.put(Types.VARCHAR, RELPIPE_TYPE_STRING);
       
    78 		RELPIPE_TYPES = Collections.unmodifiableMap(m);
       
    79 	}
       
    80 
    52 	public static final String NAME = "xml"; // bash-completion:formatter
    81 	public static final String NAME = "xml"; // bash-completion:formatter
    53 	public static final String PROPERTY_LABELED_COLUMNS = "labeledColumns";
    82 	public static final String PROPERTY_LABELED_COLUMNS = "labeledColumns";
    54 	private static final Logger log = Logger.getLogger(XmlFormatter.class.getName());
    83 	private static final Logger log = Logger.getLogger(XmlFormatter.class.getName());
    55 	private final boolean labeledColumns;
    84 	private final boolean labeledColumns;
    56 
    85 
       
    86 	private String currentDatabaseName;
       
    87 	private int statementCounter;
       
    88 	private int resultSetCounter;
       
    89 
    57 	public XmlFormatter(FormatterContext formatterContext) {
    90 	public XmlFormatter(FormatterContext formatterContext) {
    58 		super(formatterContext);
    91 		super(formatterContext);
    59 		labeledColumns = formatterContext.getProperties().getBoolean(PROPERTY_LABELED_COLUMNS, false);
    92 		labeledColumns = formatterContext.getProperties().getBoolean(PROPERTY_LABELED_COLUMNS, false);
    60 	}
    93 	}
    61 
    94 
       
    95 	private QName qname(String localPart) {
       
    96 		return new QName(Xmlns.RELPIPE, localPart);
       
    97 	}
       
    98 
       
    99 	private QName qnameDK(String localPart) {
       
   100 		return new QName(Xmlns.SQLDK, localPart, XML_NS_PREFIX_SQLDK);
       
   101 	}
       
   102 
    62 	@Override
   103 	@Override
    63 	public void writeStartBatch() {
   104 	public void writeStartBatch() {
    64 		super.writeStartBatch();
   105 		super.writeStartBatch();
    65 		printStartDocument();
   106 		printStartDocument();
    66 		printStartElement(qname("batchResult"), singleAttribute(qname("xmlns"), Xmlns.BATCH_RESULT));
   107 		Map<QName, String> attributes = new LinkedHashMap<>(2);
       
   108 		attributes.put(qname("xmlns"), Xmlns.RELPIPE);
       
   109 		attributes.put(new QName(null, XML_NS_PREFIX_SQLDK, "xmlns"), Xmlns.SQLDK);
       
   110 		printStartElement(qname("relpipe"), attributes);
       
   111 		statementCounter = 0;
       
   112 		resultSetCounter = 0;
       
   113 
    67 	}
   114 	}
    68 
   115 
    69 	@Override
   116 	@Override
    70 	public void writeEndBatch() {
   117 	public void writeEndBatch() {
    71 		super.writeEndBatch();
   118 		super.writeEndBatch();
    74 	}
   121 	}
    75 
   122 
    76 	@Override
   123 	@Override
    77 	public void writeStartDatabase(DatabaseDefinition databaseDefinition) {
   124 	public void writeStartDatabase(DatabaseDefinition databaseDefinition) {
    78 		super.writeStartDatabase(databaseDefinition);
   125 		super.writeStartDatabase(databaseDefinition);
       
   126 		currentDatabaseName = databaseDefinition.getName();
    79 		Map<QName, String> attributes = databaseDefinition.getName() == null ? null : singleAttribute(qname("name"), databaseDefinition.getName());
   127 		Map<QName, String> attributes = databaseDefinition.getName() == null ? null : singleAttribute(qname("name"), databaseDefinition.getName());
    80 		printStartElement(qname("database"), attributes);
   128 		printEmptyElement(qnameDK("database-start"), attributes);
    81 	}
   129 	}
    82 
   130 
    83 	@Override
   131 	@Override
    84 	public void writeEndDatabase() {
   132 	public void writeEndDatabase() {
    85 		super.writeEndDatabase();
   133 		super.writeEndDatabase();
    86 		printEndElement();
   134 		Map<QName, String> attributes = currentDatabaseName == null ? null : singleAttribute(qname("name"), currentDatabaseName);
       
   135 		printEmptyElement(qnameDK("database-end"), attributes);
       
   136 	}
       
   137 
       
   138 	private String getCurrentStatementName() {
       
   139 		return "s" + statementCounter;
    87 	}
   140 	}
    88 
   141 
    89 	@Override
   142 	@Override
    90 	public void writeStartStatement() {
   143 	public void writeStartStatement() {
    91 		super.writeStartStatement();
   144 		super.writeStartStatement();
    92 		printStartElement(qname("statement"));
   145 		statementCounter++;
       
   146 		printEmptyElement(qnameDK("statement-start"), singleAttribute(qname("id"), getCurrentStatementName()));
    93 	}
   147 	}
    94 
   148 
    95 	@Override
   149 	@Override
    96 	public void writeEndStatement() {
   150 	public void writeEndStatement() {
    97 		super.writeEndStatement();
   151 		super.writeEndStatement();
    98 		printEndElement();
   152 		printEmptyElement(qnameDK("statement-end"), singleAttribute(qname("id"), getCurrentStatementName()));
    99 	}
   153 	}
   100 
   154 
   101 	@Override
   155 	@Override
   102 	public void writeQuery(String sql) {
   156 	public void writeQuery(String sql) {
   103 		super.writeQuery(sql);
   157 		super.writeQuery(sql);
   104 		printTextElement(qname("sql"), null, sql);
   158 		printTextElement(qnameDK("sql"), singleAttribute(qname(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()), sql);
   105 	}
   159 	}
   106 
   160 
   107 	@Override
   161 	@Override
   108 	public void writeParameters(List<? extends Parameter> parameters) {
   162 	public void writeParameters(List<? extends Parameter> parameters) {
   109 		super.writeParameters(parameters);
   163 		super.writeParameters(parameters);
   110 
   164 
   111 		for (Parameter p : notNull(parameters)) {
   165 		if (parameters != null && parameters.size() > 0) {
   112 
   166 
   113 			Map<QName, String> attributes = new LinkedHashMap<>(2);
   167 			printStartElement(qnameDK("parameters"), singleAttribute(qname(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()));
   114 			if (p instanceof NamedParameter) {
   168 
   115 				attributes.put(qname("name"), ((NamedParameter) p).getName());
   169 			for (Parameter p : notNull(parameters)) {
   116 			}
   170 
   117 			attributes.put(qname("type"), p.getType().name());
   171 				Map<QName, String> attributes = new LinkedHashMap<>(2);
   118 
   172 				if (p instanceof NamedParameter) {
   119 			printTextElement(qname("parameter"), attributes, String.valueOf(p.getValue()));
   173 					attributes.put(qname(XML_ATTRIBUTE_NAME), ((NamedParameter) p).getName());
   120 		}
   174 				}
   121 
   175 				attributes.put(qname(XML_ATTRIBUTE_TYPE), p.getType().name());
       
   176 
       
   177 				printTextElement(qnameDK("parameter"), attributes, String.valueOf(p.getValue()));
       
   178 			}
       
   179 
       
   180 			printEndElement();
       
   181 
       
   182 		}
       
   183 
       
   184 	}
       
   185 
       
   186 	private String getCurrentRelationName() {
       
   187 		// TODO: support custom names (add CLI option)
       
   188 		return "r" + resultSetCounter;
   122 	}
   189 	}
   123 
   190 
   124 	@Override
   191 	@Override
   125 	public void writeStartResultSet(ColumnsHeader header) {
   192 	public void writeStartResultSet(ColumnsHeader header) {
   126 		super.writeStartResultSet(header);
   193 		super.writeStartResultSet(header);
   127 		printStartElement(qname("resultSet"));
   194 		resultSetCounter++;
   128 
   195 		printStartElement(qname(XML_ELEMENT_RELATION), singleAttribute(qnameDK(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()));
       
   196 		printTextElement(qname(XML_ELEMENT_NAME), null, getCurrentRelationName());
       
   197 
       
   198 		printStartElement(qname("attributes-metadata"));
   129 		for (ColumnDescriptor cd : header.getColumnDescriptors()) {
   199 		for (ColumnDescriptor cd : header.getColumnDescriptors()) {
   130 			Map<QName, String> attributes = new LinkedHashMap<>(4);
   200 			Map<QName, String> attributes = new LinkedHashMap<>(6);
   131 			attributes.put(qname("label"), cd.getLabel());
   201 
   132 			attributes.put(qname("name"), cd.getName());
   202 			attributes.put(qname(XML_ATTRIBUTE_NAME), cd.getLabel());
   133 			attributes.put(qname("typeName"), cd.getTypeName());
   203 			attributes.put(qname(XML_ATTRIBUTE_TYPE), toRelpipeType(cd.getType()));
   134 			attributes.put(qname("type"), String.valueOf(cd.getType()));
   204 
   135 			printEmptyElement(qname("columnHeader"), attributes);
   205 			attributes.put(qnameDK("tableName"), cd.getTableName());
   136 		}
   206 			attributes.put(qnameDK("columnName"), cd.getName());
       
   207 			attributes.put(qnameDK("jdbcTypeName"), cd.getTypeName());
       
   208 			attributes.put(qnameDK("jdbcType"), String.valueOf(cd.getType()));
       
   209 
       
   210 			printEmptyElement(qname("attribute-metadata"), attributes);
       
   211 		}
       
   212 		printEndElement();
       
   213 	}
       
   214 
       
   215 	/**
       
   216 	 * @param jdbcType value from {@linkplain Types}
       
   217 	 * @return
       
   218 	 */
       
   219 	private String toRelpipeType(int jdbcType) {
       
   220 		return RELPIPE_TYPES.getOrDefault(jdbcType, RELPIPE_TYPE_STRING);
   137 	}
   221 	}
   138 
   222 
   139 	@Override
   223 	@Override
   140 	public void writeEndResultSet() {
   224 	public void writeEndResultSet() {
   141 		super.writeEndResultSet();
   225 		super.writeEndResultSet();
   143 	}
   227 	}
   144 
   228 
   145 	@Override
   229 	@Override
   146 	public void writeStartRow() {
   230 	public void writeStartRow() {
   147 		super.writeStartRow();
   231 		super.writeStartRow();
   148 		printStartElement(qname("row"));
   232 		printStartElement(qname(XML_ELEMENT_RECORD));
   149 	}
   233 	}
   150 
   234 
   151 	@Override
   235 	@Override
   152 	public void writeColumnValue(Object value) {
   236 	public void writeColumnValue(Object value) {
   153 		super.writeColumnValue(value);
   237 		super.writeColumnValue(value);
   154 
   238 
   155 		Map<QName, String> attributes = null;
   239 		Map<QName, String> attributes = null;
   156 		if (labeledColumns) {
   240 		if (labeledColumns) {
   157 			attributes = new LinkedHashMap<>(2);
   241 			attributes = new LinkedHashMap<>(2);
   158 			attributes.put(qname("label"), getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getLabel());
   242 			attributes.put(qname(XML_ATTRIBUTE_NAME), getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getLabel());
       
   243 			attributes.put(qname(XML_ATTRIBUTE_TYPE), toRelpipeType(getCurrentColumnsHeader().getColumnDescriptors().get(getCurrentColumnsCount() - 1).getType()));
   159 		}
   244 		}
   160 
   245 
   161 		if (value == null) {
   246 		if (value == null) {
   162 			if (attributes == null) {
   247 			if (attributes == null) {
   163 				attributes = new LinkedHashMap<>(2);
   248 				attributes = new LinkedHashMap<>(2);
   164 			}
   249 			}
   165 			attributes.put(qname("null"), "true");
   250 			// TODO: synchronize syntax with Relational pipes (after adding support of null values)
   166 			printEmptyElement(qname("column"), attributes);
   251 			attributes.put(qnameDK("null"), "true");
       
   252 			printEmptyElement(qname(XML_ELEMENT_ATTRIBUTE), attributes);
   167 		} else if (value instanceof Array) {
   253 		} else if (value instanceof Array) {
   168 
   254 
   169 			Array sqlArray = (Array) value;
   255 			Array sqlArray = (Array) value;
   170 			try {
   256 			try {
   171 				Object[] array = (Object[]) sqlArray.getArray();
   257 				Object[] array = (Object[]) sqlArray.getArray();
   172 				printStartElement(qname("column"), attributes);
   258 				printStartElement(qname(XML_ELEMENT_ATTRIBUTE), attributes);
   173 				printArray(array);
   259 				printArray(array);
   174 				printEndElement();
   260 				printEndElement();
   175 			} catch (SQLException e) {
   261 			} catch (SQLException e) {
   176 				// FIXME: rewrite array formatting, remember array mode, don't try sqlArray.getArray() again and again if it has failed
   262 				// FIXME: rewrite array formatting, remember array mode, don't try sqlArray.getArray() again and again if it has failed
   177 				log.log(Level.SEVERE, "Unable to format array", e);
   263 				log.log(Level.SEVERE, "Unable to format array", e);
   184 						// for (int i = 1; i <= columnCount; i++) {
   270 						// for (int i = 1; i <= columnCount; i++) {
   185 						// 	log.log(Level.INFO, "Array column {0} = {1}", new Object[]{i, arrayResultSet.getObject(i)});
   271 						// 	log.log(Level.INFO, "Array column {0} = {1}", new Object[]{i, arrayResultSet.getObject(i)});
   186 						// }
   272 						// }
   187 					}
   273 					}
   188 
   274 
   189 					printStartElement(qname("column"), attributes);
   275 					printStartElement(qname(XML_ELEMENT_ATTRIBUTE), attributes);
   190 					// FIXME: instanceof SQLXML, see below
   276 					// FIXME: instanceof SQLXML, see below
   191 					printArray(arrayList.toArray());
   277 					printArray(arrayList.toArray());
   192 					printEndElement();
   278 					printEndElement();
   193 
   279 
   194 				} catch (SQLException e2) {
   280 				} catch (SQLException e2) {
   201 
   287 
   202 		} else if (value instanceof SQLXML) { // FIXME: move to separate method, to AbstractFormatter?
   288 		} else if (value instanceof SQLXML) { // FIXME: move to separate method, to AbstractFormatter?
   203 			SQLXML xml = (SQLXML) value;
   289 			SQLXML xml = (SQLXML) value;
   204 			// TODO: parse DOM/SAX and transplant XML, don't escape (optional)
   290 			// TODO: parse DOM/SAX and transplant XML, don't escape (optional)
   205 			try {
   291 			try {
   206 				printTextElement(qname("column"), attributes, xml.getString());
   292 				printTextElement(qname(XML_ELEMENT_ATTRIBUTE), attributes, xml.getString());
   207 			} catch (SQLException e) {
   293 			} catch (SQLException e) {
   208 				log.log(Level.SEVERE, "Unable to format XML", e);
   294 				log.log(Level.SEVERE, "Unable to format XML", e);
   209 				writeColumnValue(String.valueOf(value));
   295 				writeColumnValue(String.valueOf(value));
   210 			}
   296 			}
   211 		} else {
   297 		} else {
   212 			printTextElement(qname("column"), attributes, toString(value));
   298 			printTextElement(qname(XML_ELEMENT_ATTRIBUTE), attributes, toString(value));
   213 		}
   299 		}
   214 	}
   300 	}
   215 
   301 
   216 	private void printArray(Object[] array) {
   302 	private void printArray(Object[] array) {
   217 		printStartElement(qname("array"));
   303 		// TODO: synchronize array syntax with Relational pipes
       
   304 		printStartElement(qnameDK("array"));
   218 		for (Object o : array) {
   305 		for (Object o : array) {
   219 			if (o instanceof Object[]) {
   306 			if (o instanceof Object[]) {
   220 				printStartElement(qname("item"));
   307 				printStartElement(qnameDK("item"));
   221 				printArray((Object[]) o);
   308 				printArray((Object[]) o);
   222 				printEndElement();
   309 				printEndElement();
   223 			} else {
   310 			} else {
   224 				printTextElement(qname("item"), null, String.valueOf(o));
   311 				printTextElement(qnameDK("item"), null, String.valueOf(o));
   225 			}
   312 			}
   226 		}
   313 		}
   227 		printEndElement();
   314 		printEndElement();
   228 	}
   315 	}
   229 
   316 
   234 	}
   321 	}
   235 
   322 
   236 	@Override
   323 	@Override
   237 	public void writeUpdatesResult(int updatedRowsCount) {
   324 	public void writeUpdatesResult(int updatedRowsCount) {
   238 		super.writeUpdatesResult(updatedRowsCount);
   325 		super.writeUpdatesResult(updatedRowsCount);
   239 		printTextElement(qname("updatedRows"), null, String.valueOf(updatedRowsCount));
   326 		printTextElement(qnameDK("updatedRecords"), singleAttribute(qnameDK(XML_ATTRIBUTE_STATEMENT), getCurrentStatementName()), String.valueOf(updatedRowsCount));
   240 	}
   327 	}
   241 
   328 
   242 	protected String toString(Object value) {
   329 	protected String toString(Object value) {
   243 		return String.valueOf(value);
   330 		return String.valueOf(value);
   244 	}
   331 	}