diff --git a/jOOQ/src/main/java/org/jooq/XMLFormat.java b/jOOQ/src/main/java/org/jooq/XMLFormat.java index fad3553f78..1806c24374 100644 --- a/jOOQ/src/main/java/org/jooq/XMLFormat.java +++ b/jOOQ/src/main/java/org/jooq/XMLFormat.java @@ -546,7 +546,9 @@ public final class XMLFormat { ABSENT_ELEMENT, /** - * A null value is represented by a xsi:nil="true" attribute. + * A null value is represented by a + * xsi:nil="true" attribute if {@link XMLFormat#xmlns()} is + * set, or nil="true", if it is not set. */ XSI_NIL } diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java b/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java index a4d7853d21..07acd72d80 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java @@ -123,7 +123,8 @@ import org.xml.sax.helpers.DefaultHandler; */ abstract class AbstractResult extends AbstractFormattable implements FieldsTrait, Iterable { - final AbstractRow fields; + private static final String XSI_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"; + final AbstractRow fields; AbstractResult(Configuration configuration, AbstractRow row) { super(configuration); @@ -804,7 +805,11 @@ abstract class AbstractResult extends AbstractFormattable impl if (format.xmlns()) { format = format.xmlns(false); writer.append(" xmlns=\"" + Constants.NS_EXPORT + "\""); + + if (format.nullFormat() == XSI_NIL) + writer.append(" xsi:xmlns=\"" + XSI_SCHEMA + "\""); } + writer.append(">"); if (format.header()) { @@ -878,6 +883,9 @@ abstract class AbstractResult extends AbstractFormattable impl if (format.xmlns()) { format = format.xmlns(false); writer.append(" xmlns=\"" + Constants.NS_EXPORT + "\""); + + if (format.nullFormat() == XSI_NIL) + writer.append(" xsi:xmlns=\"" + XSI_SCHEMA + "\""); } if (record == null) { @@ -906,7 +914,7 @@ abstract class AbstractResult extends AbstractFormattable impl if (value == null) { if (format.nullFormat() == XSI_NIL) - writer.append(" xsi:nil=\"true\""); + writer.append(" ").append(nil(format)).append("=\"true\""); writer.append("/>"); } @@ -943,7 +951,7 @@ abstract class AbstractResult extends AbstractFormattable impl if (o == null) { if (format.nullFormat() == XSI_NIL) - writer.append(" xsi:nil=\"true\""); + writer.append(" ").append(nil(format)).append("=\"true\""); writer.append("/>"); } @@ -1233,11 +1241,12 @@ abstract class AbstractResult extends AbstractFormattable impl Element eResult = document.createElement("result"); - if (format.xmlns()) + if (format.xmlns()) { eResult.setAttribute("xmlns", Constants.NS_EXPORT); - if (format.nullFormat() == XSI_NIL) - eResult.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + if (format.nullFormat() == XSI_NIL) + eResult.setAttribute("xmlns:xsi", XSI_SCHEMA); + } document.appendChild(eResult); @@ -1318,7 +1327,7 @@ abstract class AbstractResult extends AbstractFormattable impl ) { if (value == null) { if (format.nullFormat() == XSI_NIL) - eParent.setAttribute("xsi:nil", "true"); + eParent.setAttribute(nil(format), "true"); } else if (value instanceof Formattable f) { Document d = f.intoXML(format); @@ -1338,7 +1347,7 @@ abstract class AbstractResult extends AbstractFormattable impl if (o == null) { if (format.nullFormat() == XSI_NIL) - eElement.setAttribute("xsi:nil", "true"); + eElement.setAttribute(nil(format), "true"); } else intoXMLContent(format, builder, document, o, eElement); @@ -1357,7 +1366,11 @@ abstract class AbstractResult extends AbstractFormattable impl eParent.setTextContent(format0(value, false, false)); } - private final Node childElement(Node n) { + private static final String nil(XMLFormat format) { + return format.xmlns() ? "xsi:nil" : "nil"; + } + + private static final Node childElement(Node n) { NodeList l = n.getChildNodes(); for (int i = 0; i < l.getLength(); i++) { @@ -1431,9 +1444,13 @@ abstract class AbstractResult extends AbstractFormattable impl handler.startDocument(); - if (format.xmlns()) + if (format.xmlns()) { handler.startPrefixMapping("", Constants.NS_EXPORT); + if (format.nullFormat() == XSI_NIL) + handler.startPrefixMapping("xsi", XSI_SCHEMA); + } + handler.startElement("", "", "result", empty); if (format.header()) { handler.startElement("", "", "fields", empty); @@ -1501,8 +1518,12 @@ abstract class AbstractResult extends AbstractFormattable impl if (format.header()) handler.endElement("", "", "records"); - if (format.xmlns()) + if (format.xmlns()) { + if (format.nullFormat() == XSI_NIL) + handler.endPrefixMapping("xsi"); + handler.endPrefixMapping(""); + } handler.endDocument(); return handler; @@ -1564,7 +1585,7 @@ abstract class AbstractResult extends AbstractFormattable impl return formatted; } - private static final String escapeXML(String string) { + static final String escapeXML(String string) { return StringUtils.replaceEach(string, new String[] { "\"", "'", "<", ">", "&" }, new String[] { """, "'", "<", ">", "&"}); diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java index 77510049e1..3c55aa7dc7 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java @@ -107,6 +107,7 @@ import static org.jooq.impl.DSL.inline; import static org.jooq.impl.DSL.log; import static org.jooq.impl.DSL.name; import static org.jooq.impl.DSL.using; +import static org.jooq.impl.DSL.xmlserializeContent; import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.REQUIRES_LITERAL_CAST; import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.infinity; import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.nan; @@ -146,11 +147,14 @@ import static org.jooq.impl.Keywords.K_TRUE; import static org.jooq.impl.Keywords.K_YEAR_TO_DAY; import static org.jooq.impl.Keywords.K_YEAR_TO_FRACTION; import static org.jooq.impl.Names.N_BYTEA; +import static org.jooq.impl.Names.N_CREATEXML; +import static org.jooq.impl.Names.N_HEX; import static org.jooq.impl.Names.N_JSON_PARSE; import static org.jooq.impl.Names.N_PARSE_JSON; import static org.jooq.impl.Names.N_ST_GEOMFROMTEXT; import static org.jooq.impl.Names.N_ST_GEOMFROMWKB; import static org.jooq.impl.Names.N_TO_BINARY; +import static org.jooq.impl.Names.N_XMLTYPE; import static org.jooq.impl.R2DBC.isR2dbc; import static org.jooq.impl.SQLDataType.BIGINT; import static org.jooq.impl.SQLDataType.BLOB; @@ -333,6 +337,8 @@ import org.jooq.types.YearToMonth; import org.jooq.types.YearToSecond; import org.jooq.util.postgres.PostgresUtils; +import org.jetbrains.annotations.Nullable; + // ... // ... @@ -934,6 +940,15 @@ public class DefaultBinding implements Binding { } } + + + + + + + + + if (dataType.isUUID()) { switch (ctx.family()) { @@ -1220,7 +1235,7 @@ public class DefaultBinding implements Binding { private final void sql(BindingSQLContext ctx, T value) throws SQLException { if (ctx.render().paramType() == INLINED) if (value == null) - ctx.render().visit(K_NULL); + sqlInlineNull0(ctx); else sqlInline0(ctx, value); else @@ -1330,7 +1345,10 @@ public class DefaultBinding implements Binding { ctx.statement().registerOutParameter(ctx.index(), sqltype(ctx.statement(), ctx.configuration())); } - @SuppressWarnings("unused") + /* non-final */ void sqlInlineNull0(BindingSQLContext ctx) { + ctx.render().visit(K_NULL); + } + /* non-final */ void sqlInline0(BindingSQLContext ctx, T value) throws SQLException { sqlInline1(ctx, value); } @@ -6554,6 +6572,54 @@ public class DefaultBinding implements Binding { super(dataType, converter); } + @Override + final void sqlInlineNull0(BindingSQLContext ctx) { + switch (ctx.family()) { + + + + + + default: + super.sqlInlineNull0(ctx); + break; + } + } + + @Override + final void sqlInline0(BindingSQLContext ctx, XML value) throws SQLException { + switch (ctx.family()) { + + + + + + + + + default: + super.sqlInline0(ctx, value); + break; + } + } + + @Override + final void sqlBind0(BindingSQLContext ctx, XML value) throws SQLException { + switch (ctx.family()) { + + + + + + + + + default: + super.sqlBind0(ctx, value); + break; + } + } + @Override final void setNull0(BindingSetStatementContext ctx) throws SQLException { diff --git a/jOOQ/src/main/java/org/jooq/impl/Multiset.java b/jOOQ/src/main/java/org/jooq/impl/Multiset.java index 950c27761b..a2a8db4c61 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Multiset.java +++ b/jOOQ/src/main/java/org/jooq/impl/Multiset.java @@ -628,6 +628,12 @@ final class Multiset extends AbstractField> implemen + + + + + + diff --git a/jOOQ/src/main/java/org/jooq/impl/Names.java b/jOOQ/src/main/java/org/jooq/impl/Names.java index 611a4cac01..68d206a08e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Names.java +++ b/jOOQ/src/main/java/org/jooq/impl/Names.java @@ -97,6 +97,7 @@ final class Names { static final Name N_COUNT_IF = systemName("count_if"); static final Name N_covarPop = systemName("covarPop"); static final Name N_covarSamp = systemName("covarSamp"); + static final Name N_CREATEXML = systemName("createxml"); static final Name N_CUBE = systemName("cube"); static final Name N_CURRENT_BIGDATETIME = systemName("current_bigdatetime"); static final Name N_CURRENT_DATE = systemName("current_date"); diff --git a/jOOQ/src/main/java/org/jooq/impl/XMLHandler.java b/jOOQ/src/main/java/org/jooq/impl/XMLHandler.java index 79622bb10d..bce132221c 100644 --- a/jOOQ/src/main/java/org/jooq/impl/XMLHandler.java +++ b/jOOQ/src/main/java/org/jooq/impl/XMLHandler.java @@ -38,6 +38,8 @@ package org.jooq.impl; // ... +import static org.jooq.XML.xml; +import static org.jooq.impl.AbstractResult.escapeXML; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.name; import static org.jooq.impl.DefaultDataType.getDataType; @@ -51,18 +53,21 @@ import static org.jooq.impl.Tools.row0; import static org.jooq.tools.StringUtils.defaultIfBlank; import java.io.ByteArrayInputStream; +import java.io.StringWriter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Base64; import java.util.Deque; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.jooq.ContextConverter; -import org.jooq.Converter; import org.jooq.ConverterContext; import org.jooq.DSLContext; import org.jooq.DataType; @@ -71,22 +76,181 @@ import org.jooq.Record; import org.jooq.Result; import org.jooq.exception.DataAccessException; import org.jooq.tools.JooqLogger; +import org.jooq.tools.StringUtils; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.DefaultHandler; /** * @author Lukas Eder */ -final class XMLHandler extends DefaultHandler { +final class XMLHandler +extends + DefaultHandler +implements + LexicalHandler +{ private static final JooqLogger log = JooqLogger.getLogger(XMLHandler.class); private static final boolean debug = false; private final DSLContext ctx; private final Deque> states; private State s; + static class XMLWriter extends DefaultHandler implements LexicalHandler { + final StringWriter out; + int level; + String lastElement; + String[] lastAttributes; + boolean cdata; + + XMLWriter() { + out = new StringWriter(); + + // [#19229] TODO: StringWriter seems good enough for our test cases. Perhaps, switch to XMLStreamWriter, instead? + } + + private boolean flushLastElement(boolean end) { + if (lastElement != null) { + out.write('<'); + out.write(lastElement); + + if (lastAttributes != null) { + for (int i = 0; i < lastAttributes.length; i += 2) { + out.write(' '); + out.write(lastAttributes[i]); + out.write("=\""); + out.write(escapeXML(lastAttributes[i + 1])); + out.write("\""); + } + } + + if (end) + out.write("/>"); + else + out.write('>'); + + lastElement = null; + lastAttributes = null; + return true; + } + else + return false; + } + + // -------------------------------------------------------------------- + // ContentHandler API + // -------------------------------------------------------------------- + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + level++; + + flushLastElement(false); + lastElement = qName; + + // [#19229] Attributes is a mutable object in some parsers (e.g. ojdbc ships its own), + // so we have to copy its contents + if (atts != null && atts.getLength() > 0) { + lastAttributes = new String[atts.getLength() * 2]; + + for (int i = 0; i < atts.getLength(); i++) { + lastAttributes[i * 2] = atts.getQName(i); + lastAttributes[i * 2 + 1] = atts.getValue(i); + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (!flushLastElement(true)) { + out.write("'); + } + + level--; + } + + @Override + public void processingInstruction(String target, String data) throws SAXException { + flushLastElement(false); + out.write(""); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + flushLastElement(false); + + if (cdata) + out.write(ch, start, length); + else + out.write(escapeXML(new String(ch, start, length))); + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + flushLastElement(false); + out.write(ch, start, length); + } + + // -------------------------------------------------------------------- + // LexicalHandler API + // -------------------------------------------------------------------- + + @Override + public void startCDATA() throws SAXException { + cdata = true; + flushLastElement(false); + out.write(""); + cdata = false; + } + + @Override + public void comment(char[] ch, int start, int length) throws SAXException { + flushLastElement(false); + + out.write(""); + } + + // [#19229] TODO: Implement these if needed + + @Override + public void startDTD(String name, String publicId, String systemId) throws SAXException { + } + + @Override + public void endDTD() throws SAXException { + } + + @Override + public void startEntity(String name) throws SAXException { + } + + @Override + public void endEntity(String name) throws SAXException { + } + } + private static class State { final DSLContext ctx; AbstractRow row; @@ -101,6 +265,7 @@ final class XMLHandler extends DefaultHandler { final List values; List elements; int column; + XMLWriter writer; @SuppressWarnings("unchecked") State(DSLContext ctx, AbstractRow row, Class recordType) { @@ -187,6 +352,11 @@ final class XMLHandler extends DefaultHandler { SAXParser saxParser = factory.newSAXParser(); // TODO: Why does the SAXParser replace \r by \n? + try { + saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", this); + } + catch (SAXNotRecognizedException | SAXNotSupportedException ignore) {} + saxParser.parse(new ByteArrayInputStream(string.getBytes(ctx.configuration().charsetProvider().provide())), this); return s.result; } @@ -202,7 +372,10 @@ final class XMLHandler extends DefaultHandler { if (log.isDebugEnabled()) log.debug("> " + qName); - if (!s.inResult && "result".equalsIgnoreCase(qName)) { + if (s.writer != null) { + s.writer.startElement(uri, localName, qName, attributes); + } + else if (!s.inResult && "result".equalsIgnoreCase(qName)) { s.inResult = true; } else if (s.inColumn && "result".equalsIgnoreCase(qName)) { @@ -265,15 +438,23 @@ final class XMLHandler extends DefaultHandler { s.inColumn = true; - DataType t = s.fields.get(s.column).getDataType(); + Field f = s.fields.get(s.column); + DataType t = f.getDataType(); // [#13181] String NULL and '' values cannot be distinguished without xsi:nil - if (t.isString() && !isNil(attributes)) + if (t.isString() && !isNil(attributes)) { s.values.add(""); - else if (t.isArray() && !isNil(attributes)) + } + else if (t.isArray() && !isNil(attributes)) { s.elements = new ArrayList<>(); - else if (!t.isMultiset() && !t.isRecord()) + } + else if (!t.isMultiset() && !t.isRecord()) { s.values.add(null); + + // [#19229] Copy XML content + if (f.getDataType().isXML() && s.writer == null) + s.writer = new XMLWriter(); + } } } @@ -291,7 +472,18 @@ final class XMLHandler extends DefaultHandler { if (log.isDebugEnabled()) log.debug("< " + qName); - if (states.isEmpty() && s.inResult && s.inRecord == 0 && "result".equalsIgnoreCase(qName)) { + if (s.writer != null && s.writer.level == 0) + s.writer = null; + + if (s.writer != null) { + s.writer.endElement(uri, localName, qName); + + if (s.writer.level == 0) { + s.values.set(s.values.size() - 1, xml(s.writer.out.toString())); + s.writer = null; + } + } + else if (states.isEmpty() && s.inResult && s.inRecord == 0 && "result".equalsIgnoreCase(qName)) { if (s.result == null) initResult(); @@ -367,11 +559,20 @@ final class XMLHandler extends DefaultHandler { return allMatch(fields, f -> "value".equalsIgnoreCase(f.getName())); } + @Override + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + if (s.writer != null) + s.writer.ignorableWhitespace(ch, start, length); + } + @Override public final void characters(char[] ch, int start, int length) throws SAXException { DataType t; - if (s.inColumn + if (s.writer != null) { + s.writer.characters(ch, start, length); + } + else if (s.inColumn && !(t = s.fields.get(s.column).getDataType()).isRecord() && !t.isMultiset() && (!t.isArray() || s.inElement) @@ -390,4 +591,46 @@ final class XMLHandler extends DefaultHandler { s.values.set(s.column, old + value); } } + + @Override + public void startDTD(String name, String publicId, String systemId) throws SAXException { + if (s != null && s.writer != null) + s.writer.startDTD(name, publicId, systemId); + } + + @Override + public void endDTD() throws SAXException { + if (s != null && s.writer != null) + s.writer.endDTD(); + } + + @Override + public void startEntity(String name) throws SAXException { + if (s != null && s.writer != null) + s.writer.startEntity(name); + } + + @Override + public void endEntity(String name) throws SAXException { + if (s != null && s.writer != null) + s.writer.endEntity(name); + } + + @Override + public void startCDATA() throws SAXException { + if (s != null && s.writer != null) + s.writer.startCDATA(); + } + + @Override + public void endCDATA() throws SAXException { + if (s != null && s.writer != null) + s.writer.endCDATA(); + } + + @Override + public void comment(char[] ch, int start, int length) throws SAXException { + if (s != null && s.writer != null) + s.writer.comment(ch, start, length); + } }