001/* 002 * Copyright (c) 2018-2025 Tada AB and other contributors, as listed below. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the The BSD 3-Clause License 006 * which accompanies this distribution, and is available at 007 * http://opensource.org/licenses/BSD-3-Clause 008 * 009 * Contributors: 010 * Chapman Flack 011 */ 012package org.postgresql.pljava.example.annotation; 013 014import java.sql.Connection; 015import java.sql.DriverManager; 016import java.sql.PreparedStatement; 017import java.sql.ResultSet; 018import java.sql.ResultSetMetaData; 019import java.sql.SQLData; 020import java.sql.SQLInput; 021import java.sql.SQLOutput; 022import java.sql.SQLXML; 023import java.sql.Statement; 024import java.sql.Types; 025 026import java.sql.SQLDataException; 027import java.sql.SQLException; 028 029import java.io.ByteArrayInputStream; 030import java.io.InputStream; 031import java.io.OutputStream; 032import java.io.Reader; 033import java.io.StringReader; 034import java.io.StringWriter; 035import java.io.Writer; 036 037import java.io.IOException; 038 039import java.util.List; 040import java.util.Map; 041import java.util.HashMap; 042 043import javax.xml.parsers.DocumentBuilderFactory; 044import javax.xml.parsers.ParserConfigurationException; 045 046import static javax.xml.transform.OutputKeys.ENCODING; 047import javax.xml.transform.Result; 048import javax.xml.transform.Source; 049import javax.xml.transform.Templates; 050import javax.xml.transform.Transformer; 051import javax.xml.transform.TransformerFactory; 052 053import javax.xml.transform.TransformerException; 054import javax.xml.transform.TransformerConfigurationException; 055 056import javax.xml.transform.stream.StreamResult; 057import javax.xml.transform.stream.StreamSource; 058import javax.xml.transform.dom.DOMResult; 059import javax.xml.transform.dom.DOMSource; 060import javax.xml.transform.sax.SAXResult; 061import javax.xml.transform.sax.SAXSource; 062import javax.xml.transform.stax.StAXResult; 063import javax.xml.transform.stax.StAXSource; 064 065import javax.xml.validation.Schema; 066import javax.xml.validation.SchemaFactory; 067 068import org.postgresql.pljava.Adjusting; 069import static org.postgresql.pljava.Adjusting.XML.setFirstSupported; 070import org.postgresql.pljava.SessionManager; 071import org.postgresql.pljava.annotation.Function; 072import org.postgresql.pljava.annotation.MappedUDT; 073import org.postgresql.pljava.annotation.SQLAction; 074import org.postgresql.pljava.annotation.SQLType; 075 076import static org.postgresql.pljava.example.LoggerTest.logMessage; 077 078/* Imports needed just for the SAX flavor of "low-level XML echo" below */ 079import javax.xml.parsers.ParserConfigurationException; 080import javax.xml.parsers.SAXParserFactory; 081import org.xml.sax.XMLReader; 082import org.xml.sax.ContentHandler; 083import org.xml.sax.DTDHandler; 084import org.xml.sax.ext.LexicalHandler; 085 086/* Imports needed just for the StAX flavor of "low-level XML echo" below */ 087import javax.xml.stream.XMLEventReader; 088import javax.xml.stream.XMLEventWriter; 089import javax.xml.stream.XMLInputFactory; 090import javax.xml.stream.XMLOutputFactory; 091import javax.xml.stream.XMLStreamException; 092import org.xml.sax.SAXException; 093 094/* Imports needed just for xmlTextNode below (serializing via SAX, StAX, DOM) */ 095import org.xml.sax.helpers.AttributesImpl; 096import org.w3c.dom.Document; 097import org.w3c.dom.DocumentFragment; 098import org.w3c.dom.bootstrap.DOMImplementationRegistry; 099 100 101/** 102 * Class illustrating use of {@link SQLXML} to operate on XML data. 103 *<p> 104 * This class also serves as the mapping class for a composite type 105 * {@code javatest.onexml}, the better to verify that {@link SQLData} 106 * input/output works too. That's why it has to implement SQLData. 107 *<p> 108 * Everything mentioning the type XML here needs a conditional implementor tag 109 * in case of being loaded into a PostgreSQL instance built without that type. 110 */ 111@SQLAction(provides="postgresql_xml", install= 112 "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + 113 " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + 114 " current_setting('pljava.implementors'), true) " + 115 "END" 116) 117 118@SQLAction(implementor="postgresql_xml", requires="echoXMLParameter", 119 install= 120 "WITH" + 121 " s(how) AS (SELECT generate_series(1, 7))," + 122 " t(x) AS (" + 123 " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + 124 " )," + 125 " r(howin, howout, isdoc) AS (" + 126 " SELECT" + 127 " i.how, o.how," + 128 " javatest.echoxmlparameter(x, i.how, o.how) IS DOCUMENT" + 129 " FROM" + 130 " t, s AS i, s AS o" + 131 " WHERE" + 132 " NOT (i.how = 6 and o.how = 7)" + // 6->7 unreliable in some JREs 133 " ) " + 134 "SELECT" + 135 " CASE WHEN every(isdoc)" + 136 " THEN javatest.logmessage('INFO', 'SQLXML echos succeeded')" + 137 " ELSE javatest.logmessage('WARNING', 'SQLXML echos had problems')" + 138 " END " + 139 "FROM" + 140 " r" 141) 142 143@SQLAction(implementor="postgresql_xml", requires="proxiedXMLEcho", 144 install= 145 "WITH" + 146 " s(how) AS (SELECT unnest('{1,2,4,5,6,7}'::int[]))," + 147 " t(x) AS (" + 148 " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + 149 " )," + 150 " r(how, isdoc) AS (" + 151 " SELECT" + 152 " how," + 153 " javatest.proxiedxmlecho(x, how) IS DOCUMENT" + 154 " FROM" + 155 " t, s" + 156 " )" + 157 "SELECT" + 158 " CASE WHEN every(isdoc)" + 159 " THEN javatest.logmessage('INFO', 'proxied SQLXML echos succeeded')" + 160 " ELSE javatest.logmessage('WARNING'," + 161 " 'proxied SQLXML echos had problems')" + 162 " END " + 163 "FROM" + 164 " r" 165) 166 167@SQLAction(implementor="postgresql_xml", requires="lowLevelXMLEcho", 168 install={ 169 "SELECT" + 170 " preparexmlschema('schematest', $$" + 171 "<xs:schema" + 172 " xmlns:xs='http://www.w3.org/2001/XMLSchema'" + 173 " targetNamespace='urn:testme'" + 174 " elementFormDefault='qualified'>" + 175 " <xs:element name='row'>" + 176 " <xs:complexType>" + 177 " <xs:sequence>" + 178 " <xs:element name='textcol' type='xs:string' nillable='true'/>" + 179 " <xs:element name='intcol' type='xs:integer' nillable='true'/>" + 180 " </xs:sequence>" + 181 " </xs:complexType>" + 182 " </xs:element>" + 183 "</xs:schema>" + 184 "$$, 'http://www.w3.org/2001/XMLSchema', 5)", 185 186 "WITH" + 187 " s(how) AS (SELECT unnest('{4,5,7}'::int[]))," + 188 " r(isdoc) AS (" + 189 " SELECT" + 190 " javatest.lowlevelxmlecho(" + 191 " query_to_xml(" + 192 " 'SELECT ''hi'' AS textcol, 1 AS intcol', true, true, 'urn:testme'"+ 193 " ), how, params) IS DOCUMENT" + 194 " FROM" + 195 " s," + 196 " (SELECT 'schematest' AS schema) AS params" + 197 " )" + 198 "SELECT" + 199 " CASE WHEN every(isdoc)" + 200 " THEN javatest.logmessage('INFO', 'XML Schema tests succeeded')" + 201 " ELSE javatest.logmessage('WARNING'," + 202 " 'XML Schema tests had problems')" + 203 " END " + 204 "FROM" + 205 " r" 206 } 207) 208 209@SQLAction(implementor="postgresql_xml", 210 requires={"prepareXMLTransform", "transformXML"}, 211 install={ 212 "REVOKE EXECUTE ON FUNCTION javatest.prepareXMLTransformWithJava" + 213 " (pg_catalog.varchar, pg_catalog.xml, integer, boolean, boolean," + 214 " pg_catalog.RECORD)" + 215 " FROM PUBLIC", 216 217 "SELECT" + 218 " javatest.prepareXMLTransform('distinctElementNames'," + 219 "'<xsl:transform version=''1.0''" + 220 " xmlns:xsl=''http://www.w3.org/1999/XSL/Transform''" + 221 " xmlns:exsl=''http://exslt.org/common''" + 222 " xmlns:set=''http://exslt.org/sets''" + 223 " extension-element-prefixes=''exsl set''" + 224 ">" + 225 " <xsl:output method=''xml'' indent=''no''/>" + 226 " <xsl:template match=''/''>" + 227 " <xsl:variable name=''enames''>" + 228 " <xsl:for-each select=''//*''>" + 229 " <ename><xsl:value-of select=''local-name()''/></ename>" + 230 " </xsl:for-each>" + 231 " </xsl:variable>" + 232 " <xsl:for-each" + 233 " select=''set:distinct(exsl:node-set($enames)/ename)''>" + 234 " <xsl:sort select=''string()''/>" + 235 " <den><xsl:value-of select=''.''/></den>" + 236 " </xsl:for-each>" + 237 " </xsl:template>" + 238 "</xsl:transform>', how => 5, enableExtensionFunctions => true)", 239 240 "SELECT" + 241 " javatest.prepareXMLTransformWithJava('getPLJavaVersion'," + 242 "'<xsl:transform version=''1.0''" + 243 " xmlns:xsl=''http://www.w3.org/1999/XSL/Transform''" + 244 " xmlns:java=''http://xml.apache.org/xalan/java''" + 245 " exclude-result-prefixes=''java''" + 246 ">" + 247 " <xsl:template match=''/''>" + 248 " <xsl:value-of" + 249 " select=''java:java.lang.System.getProperty(" + 250 " \"org.postgresql.pljava.version\")''" + 251 " />" + 252 " </xsl:template>" + 253 "</xsl:transform>', enableExtensionFunctions => true)", 254 255 "SELECT" + 256 " CASE WHEN" + 257 " javatest.transformXML('distinctElementNames'," + 258 " '<a><c/><e/><b/><b/><d/></a>', 5, 5)::text" + 259 " =" + 260 " '<den>a</den><den>b</den><den>c</den><den>d</den><den>e</den>'"+ 261 " THEN javatest.logmessage('INFO', 'XSLT 1.0 test succeeded')" + 262 " ELSE javatest.logmessage('WARNING', 'XSLT 1.0 test failed')" + 263 " END", 264 265 "SELECT" + 266 " CASE WHEN" + 267 " javatest.transformXML('getPLJavaVersion', '')::text" + 268 " OPERATOR(pg_catalog.=) extversion" + 269 " THEN javatest.logmessage('INFO', 'XSLT 1.0 with Java succeeded')" + 270 " ELSE javatest.logmessage('WARNING', 'XSLT 1.0 with Java failed')" + 271 " END" + 272 " FROM pg_catalog.pg_extension" + 273 " WHERE extname = 'pljava'" 274 } 275) 276 277@SQLAction(implementor="postgresql_xml", 278 provides="xml_java_ge_22", requires="javaSpecificationGE", install= 279 "SELECT CASE WHEN" + 280 " javatest.javaSpecificationGE('22')" + 281 " THEN set_config('pljava.implementors', 'xml_java_ge_22,' || " + 282 " current_setting('pljava.implementors'), true) " + 283 "END" 284) 285 286@SQLAction(implementor="xml_java_ge_22", requires="lowLevelXMLEcho", install= 287 "WITH" + 288 " s(how) AS (SELECT unnest('{5,6,7}'::int[]))," + 289 " r(isdoc) AS (" + 290 " SELECT" + 291 " javatest.lowlevelxmlecho(" + 292 /* 293 * A truly minimal DTD, <!DOCTYPE a>, cannot be ignored by Java 22's SAX/DOM 294 * parser (though it can be, when using the StAX API). NullPointerException 295 * calling getActiveGrammar().isImmutable() is the result. Bug: JDK-8329295 296 * Including either an externalID or an internal subset (like the empty [] 297 * here) avoids the issue. 298 */ 299 " '<!DOCTYPE a []><a/>'::xml, how, params) IS DOCUMENT" + 300 " FROM" + 301 " s," + 302 " (SELECT null::void AS ignoreDTD) AS params" + 303 " )" + 304 "SELECT" + 305 " CASE WHEN every(isdoc)" + 306 " THEN javatest.logmessage('INFO', 'jdk.xml.dtd.support=ignore OK')" + 307 " ELSE javatest.logmessage('WARNING', 'jdk.xml.dtd.support=ignore NG')" + 308 " END " + 309 "FROM" + 310 " r" 311) 312 313@MappedUDT(schema="javatest", name="onexml", structure="c1 xml", 314 implementor="postgresql_xml", 315 comment="A composite type mapped by the PassXML example class") 316public class PassXML implements SQLData 317{ 318 static SQLXML s_sx; 319 320 static TransformerFactory s_tf = TransformerFactory.newDefaultInstance(); 321 322 static Map<String,Templates> s_tpls = new HashMap<>(); 323 324 static Map<String,Schema> s_schemas = new HashMap<>(); 325 326 @Function(schema="javatest", implementor="postgresql_xml") 327 public static String inXMLoutString(SQLXML in) throws SQLException 328 { 329 return in.getString(); 330 } 331 332 @Function(schema="javatest", implementor="postgresql_xml") 333 public static SQLXML inStringoutXML(String in) throws SQLException 334 { 335 Connection c = DriverManager.getConnection("jdbc:default:connection"); 336 SQLXML result = c.createSQLXML(); 337 result.setString(in); 338 return result; 339 } 340 341 /** 342 * Echo an XML parameter back, exercising seven different ways 343 * (howin => 1-7) of reading an SQLXML object, and seven 344 * (howout => 1-7) of returning one. 345 *<p> 346 * If howin => 0, the XML parameter is simply saved in a static. It can 347 * be read in a subsequent call with sx => null, but only in the same 348 * transaction. 349 *<p> 350 * The "echoing" is done (in the {@code echoXML} method below) using a 351 * {@code Transformer}, that is, the "TrAX" Transformation API for XML 352 * supplied in Java. It illustrates how an identity {@code Transformer} can 353 * be used to get the XML content from the source to the result for any of 354 * the APIs selectable by howin and howout. 355 *<p> 356 * It also illustrates something else. When using StAX (6 for howin 357 * or howout) and XML of the {@code CONTENT} flavor (multiple top-level 358 * elements, characters outside the top element, etc.), it is easy to 359 * construct examples that fail. The fault is not really with the StAX API, 360 * nor with TrAX proper, but with the small handful of bridge classes that 361 * were added to the JRE with StAX's first appearance, to make it 362 * interoperate with TrAX. It is not that those classes completely overlook 363 * the {@code CONTENT} case: they make some efforts to handle it. Just not 364 * the right ones, and given the Java developers' usual reluctance to change 365 * such longstanding behavior, that's probably not getting fixed. 366 *<p> 367 * Moral: StAX is a nice API, have no fear to use it directly in 368 * freshly-developed code, but: when using TrAX, make every effort to supply 369 * a {@code Transformer} with {@code Source} and {@code Result} objects of 370 * <em>any</em> kind other than StAX. 371 */ 372 @Function(schema="javatest", implementor="postgresql_xml", 373 provides="echoXMLParameter") 374 public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) 375 throws SQLException 376 { 377 if ( null == sx ) 378 sx = s_sx; 379 if ( 0 == howin ) 380 { 381 s_sx = sx; 382 return null; 383 } 384 return echoSQLXML(sx, howin, howout); 385 } 386 387 /** 388 * Echo an XML parameter back, but with parameter and return types of 389 * PostgreSQL {@code text}. 390 *<p> 391 * The other version of this method needs a conditional implementor tag 392 * because it cannot be declared in a PostgreSQL instance that was built 393 * without {@code libxml} support and the PostgreSQL {@code XML} type. 394 * But this version can, simply by mapping the {@code SQLXML} parameter 395 * and return types to the SQL {@code text} type. The Java code is no 396 * different. 397 *<p> 398 * Note that it's possible for both declarations to coexist in PostgreSQL 399 * (because as far as it is concerned, their signatures are different), but 400 * these two Java methods cannot have the same name (because they differ 401 * only in annotations, not in the declared Java types). So, this one needs 402 * a slightly tweaked name, and a {@code name} attribute in the annotation 403 * so PostgreSQL sees the right name. 404 */ 405 @Function(schema="javatest", name="echoXMLParameter", type="text") 406 public static SQLXML echoXMLParameter_( 407 @SQLType("text") SQLXML sx, int howin, int howout) 408 throws SQLException 409 { 410 return echoXMLParameter(sx, howin, howout); 411 } 412 413 /** 414 * "Echo" an XML parameter not by creating a new writable {@code SQLXML} 415 * object at all, but simply returning the passed-in readable one untouched. 416 */ 417 @Function(schema="javatest", implementor="postgresql_xml") 418 public static SQLXML bounceXMLParameter(SQLXML sx) throws SQLException 419 { 420 return sx; 421 } 422 423 /** 424 * Just like {@link bounceXMLParameter} but with parameter and return typed 425 * as {@code text}, and so usable on a PostgreSQL instance lacking the XML 426 * type. 427 */ 428 @Function(schema="javatest", type="text", name="bounceXMLParameter") 429 public static SQLXML bounceXMLParameter_(@SQLType("text") SQLXML sx) 430 throws SQLException 431 { 432 return sx; 433 } 434 435 /** 436 * Just like {@link bounceXMLParameter} but with the parameter typed as 437 * {@code text} and the return type left as XML, so functions as a cast. 438 *<p> 439 * Slower than the other cases, because it must verify that the input really 440 * is XML before blindly calling it a PostgreSQL XML type. But the speed 441 * compares respectably to PostgreSQL's own CAST(text AS xml), at least for 442 * larger values; I am seeing Java pull ahead right around 32kB of XML data 443 * and beat PG by a factor of 2 or better at sizes of 1 or 2 MB. 444 * Unsurprisingly, PG has the clear advantage when values are very short. 445 */ 446 @Function(schema="javatest", implementor="postgresql_xml") 447 public static SQLXML castTextXML(@SQLType("text") SQLXML sx) 448 throws SQLException 449 { 450 return sx; 451 } 452 453 /** 454 * Precompile an XSL transform {@code source} and save it (for the 455 * current session) as {@code name}. 456 *<p> 457 * Each value of {@code how}, 1-7, selects a different way of presenting 458 * the {@code SQLXML} object to the XSL processor. 459 *<p> 460 * Passing {@code true} for {@code enableExtensionFunctions} allows the 461 * transform to use extensions that the Java XSLT implementation supports, 462 * such as functions from EXSLT. Those are disabled by default. 463 *<p> 464 * Passing {@code false} for {@code builtin} will allow a 465 * {@code TransformerFactory} other than Java's built-in one to be found 466 * using the usual search order and the context class loader (normally 467 * the PL/Java class path for the schema where this function is declared). 468 * The default of {@code true} ensures that the built-in Java XSLT 1.0 469 * implementation is used. A transformer implementation other than Xalan 470 * may not recognize the feature controlled by 471 * {@code enableExtensionFunctions}, so failure to configure that feature 472 * will be logged as a warning if {@code builtin} is {@code false}, instead 473 * of thrown as an exception. 474 *<p> 475 * Out of the box, Java's transformers only support XSLT 1.0. See the S9 476 * example for more capabilities (at the cost of downloading the Saxon jar). 477 */ 478 @Function(schema="javatest", implementor="postgresql_xml", 479 provides="prepareXMLTransform") 480 public static void prepareXMLTransform(String name, SQLXML source, 481 @SQLType(defaultValue="0") int how, 482 @SQLType(defaultValue="false") boolean enableExtensionFunctions, 483 @SQLType(defaultValue="true") boolean builtin, 484 @SQLType(defaultValue={}) ResultSet adjust) 485 throws SQLException 486 { 487 prepareXMLTransform( 488 name, source, how, enableExtensionFunctions, adjust, builtin, 489 /* withJava */ false); 490 } 491 492 /** 493 * Precompile an XSL transform {@code source} and save it (for the 494 * current session) as {@code name}, where the transform may call Java 495 * methods. 496 *<p> 497 * Otherwise identical to {@code prepareXMLTransform}, this version sets the 498 * {@code TransformerFactory}'s {@code extensionClassLoader} (to the context 499 * class loader, normally the PL/Java class path for the schema where this 500 * function is declared), so the transform will be able to use 501 * xalan's Java call syntax to call any public Java methods that would be 502 * accessible to this class. (That can make a big difference in usefulness 503 * for the otherwise rather limited XSLT 1.0.) 504 *<p> 505 * As with {@code enableExtensionFunctions}, failure by the transformer 506 * implementation to recognize or allow the {@code extensionClassLoader} 507 * property will be logged as a warning if {@code builtin} is {@code false}, 508 * rather than thrown as an exception. 509 *<p> 510 * This example function will be installed with {@code EXECUTE} permission 511 * revoked from {@code PUBLIC}, as it essentially confers the ability to 512 * create arbitrary new Java functions, so should only be granted to roles 513 * you would be willing to grant {@code USAGE ON LANGUAGE java}. 514 *<p> 515 * Because this function only prepares the transform, and 516 * {@link #transformXML transformXML} applies it, there is some division of 517 * labor in determining what limits apply to its behavior. The use of this 518 * method instead of {@code prepareXMLTransform} determines whether the 519 * transform is allowed to see external Java methods at all; it will be 520 * the policy permissions granted to {@code transformXML} that control what 521 * those methods can do when the transform is applied. For now, that method 522 * is defined in the trusted/sandboxed {@code java} language, so this 523 * function could reasonably be granted to any role with {@code USAGE} on 524 * {@code java}. If, by contrast, {@code transformXML} were declared in the 525 * 'untrusted' {@code javaU}, it would be prudent to allow only superusers 526 * access to this function, just as only they can {@code CREATE FUNCTION} in 527 * an untrusted language. 528 */ 529 @Function(schema="javatest", implementor="postgresql_xml", 530 provides="prepareXMLTransform") 531 public static void prepareXMLTransformWithJava(String name, SQLXML source, 532 @SQLType(defaultValue="0") int how, 533 @SQLType(defaultValue="false") boolean enableExtensionFunctions, 534 @SQLType(defaultValue="true") boolean builtin, 535 @SQLType(defaultValue={}) ResultSet adjust) 536 throws SQLException 537 { 538 prepareXMLTransform( 539 name, source, how, enableExtensionFunctions, adjust, builtin, 540 /* withJava */ true); 541 } 542 543 private static void prepareXMLTransform(String name, SQLXML source, int how, 544 boolean enableExtensionFunctions, ResultSet adjust, boolean builtin, 545 boolean withJava) 546 throws SQLException 547 { 548 TransformerFactory tf = 549 builtin 550 ? TransformerFactory.newDefaultInstance() 551 : TransformerFactory.newInstance(); 552 553 String legacy_pfx = "http://www.oracle.com/xml/jaxp/properties/"; 554 String java17_pfx = "jdk.xml."; 555 String exf_sfx = "enableExtensionFunctions"; 556 557 String ecl_legacy = "jdk.xml.transform.extensionClassLoader"; 558 String ecl_java17 = "jdk.xml.extensionClassLoader"; 559 560 Source src = sxToSource(source, how, adjust); 561 562 try 563 { 564 Exception e; 565 566 e = setFirstSupported(tf::setFeature, enableExtensionFunctions, 567 List.of(TransformerConfigurationException.class), null, 568 java17_pfx + exf_sfx, legacy_pfx + exf_sfx); 569 570 if ( null != e ) 571 { 572 if ( builtin ) 573 throw new SQLException( 574 "Configuring XML transformation: " + e.getMessage(), e); 575 else 576 logMessage("WARNING", 577 "non-builtin transformer: ignoring " + e.getMessage()); 578 } 579 580 if ( withJava ) 581 { 582 e = setFirstSupported(tf::setAttribute, 583 Thread.currentThread().getContextClassLoader(), 584 List.of(IllegalArgumentException.class), null, 585 ecl_java17, ecl_legacy); 586 587 if ( null != e ) 588 { 589 if ( builtin ) 590 throw new SQLException( 591 "Configuring XML transformation: " + 592 e.getMessage(), e); 593 else 594 logMessage("WARNING", 595 "non-builtin transformer: ignoring " + 596 e.getMessage()); 597 } 598 } 599 600 s_tpls.put(name, tf.newTemplates(src)); 601 } 602 catch ( TransformerException te ) 603 { 604 throw new SQLException( 605 "Preparing XML transformation: " + te.getMessage(), te); 606 } 607 } 608 609 /** 610 * Transform some XML according to a named transform prepared with 611 * {@code prepareXMLTransform}. 612 *<p> 613 * Pass null for {@code transformName} to get a plain identity transform 614 * (not such an interesting thing to do, unless you also specify indenting). 615 */ 616 @Function(schema="javatest", implementor="postgresql_xml", 617 provides="transformXML") 618 public static SQLXML transformXML( 619 String transformName, SQLXML source, 620 @SQLType(defaultValue="0") int howin, 621 @SQLType(defaultValue="0") int howout, 622 @SQLType(defaultValue={}) ResultSet adjust, 623 @SQLType(optional=true) Boolean indent, 624 @SQLType(optional=true) Integer indentWidth) 625 throws SQLException 626 { 627 Templates tpl = null == transformName? null: s_tpls.get(transformName); 628 Source src = sxToSource(source, howin, adjust); 629 630 if ( Boolean.TRUE.equals(indent) && 0 == howout ) 631 howout = 4; // transformer only indents if writing a StreamResult 632 633 Connection c = DriverManager.getConnection("jdbc:default:connection"); 634 SQLXML result = c.createSQLXML(); 635 Result rlt = sxToResult(result, howout, adjust); 636 637 try 638 { 639 Transformer t = 640 null == tpl ? s_tf.newTransformer() : tpl.newTransformer(); 641 /* 642 * For the non-SAX/StAX/DOM flavors of output, you're responsible 643 * for setting the Transformer to use the server encoding. 644 */ 645 if ( rlt instanceof StreamResult ) 646 t.setOutputProperty(ENCODING, 647 SessionManager.current().frozenSystemProperties() 648 .getProperty("org.postgresql.server.encoding")); 649 else if ( Boolean.TRUE.equals(indent) ) 650 logMessage("WARNING", 651 "indent requested, but howout specifies a non-stream " + 652 "Result type; no indenting will happen"); 653 654 if ( null != indent ) 655 t.setOutputProperty("indent", indent ? "yes" : "no"); 656 if ( null != indentWidth ) 657 t.setOutputProperty( 658 "{http://xml.apache.org/xalan}indent-amount", 659 "" + indentWidth); 660 661 t.transform(src, rlt); 662 } 663 catch ( TransformerException te ) 664 { 665 throw new SQLException("Transforming XML: " + te.getMessage(), te); 666 } 667 668 return ensureClosed(rlt, result, howout); 669 } 670 671 /** 672 * Precompile a schema {@code source} in schema language {@code lang} 673 * and save it (for the current session) as {@code name}. 674 *<p> 675 * Each value of {@code how}, 1-7, selects a different way of presenting 676 * the {@code SQLXML} object to the schema parser. 677 *<p> 678 * The {@code lang} parameter is a URI that identifies a known schema 679 * language. The only language a Java runtime is required to support is 680 * W3C XML Schema 1.0, with URI {@code http://www.w3.org/2001/XMLSchema}. 681 */ 682 @Function(schema="javatest", implementor="postgresql_xml") 683 public static void prepareXMLSchema( 684 String name, SQLXML source, String lang, int how) 685 throws SQLException 686 { 687 try 688 { 689 s_schemas.put(name, 690 SchemaFactory.newInstance(lang) 691 .newSchema(sxToSource(source, how))); 692 } 693 catch ( SAXException e ) 694 { 695 throw new SQLException( 696 "failed to prepare schema: " + e.getMessage(), e); 697 } 698 } 699 700 private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) 701 throws SQLException 702 { 703 Connection c = DriverManager.getConnection("jdbc:default:connection"); 704 SQLXML rx = c.createSQLXML(); 705 Source src = sxToSource(sx, howin); 706 Result rlt = sxToResult(rx, howout); 707 708 try 709 { 710 Transformer t = s_tf.newTransformer(); 711 /* 712 * For the non-SAX/StAX/DOM flavors of output, you're responsible 713 * for setting the Transformer to use the server encoding. 714 */ 715 if ( howout < 5 ) 716 t.setOutputProperty(ENCODING, 717 SessionManager.current().frozenSystemProperties() 718 .getProperty("org.postgresql.server.encoding")); 719 t.transform(src, rlt); 720 } 721 catch ( TransformerException te ) 722 { 723 throw new SQLException("XML transformation failed", te); 724 } 725 726 return ensureClosed(rlt, rx, howout); 727 } 728 729 /** 730 * Echo the XML parameter back, using lower-level manipulations than 731 * {@code echoXMLParameter}. 732 *<p> 733 * This illustrates how the simple use of {@code t.transform(src,rlt)} 734 * in {@code echoSQLXML} substitutes for a lot of fiddly case-by-case code, 735 * but when coding for a specific case, all the generality of {@code 736 * transform} may not be needed. It can be interesting to compare memory use 737 * when XML values are large. 738 *<p> 739 * This method has been revised to demonstrate, even for low-level 740 * manipulations, how much fiddliness can now be avoided through use of the 741 * {@link Adjusting.XML.SourceResult} class, and how to make adjustments to 742 * parsing restrictions by passing the optional row-typed parameter 743 * <em>adjust</em>, which defaults to an empty row. For example, passing 744 *<pre> 745 * adjust => (select a from 746 * (true as allowdtd, true as expandentityreferences) as a) 747 *</pre> 748 * would allow a document that contains an internal DTD subset and uses 749 * entities defined there. 750 *<p> 751 * The older, pre-{@code SourceResult} code for doing low-level XML echo 752 * has been moved to the {@code oldSchoolLowLevelEcho} method below. It can 753 * still be exercised by calling this method, explicitly passing 754 * {@code adjust => NULL}. 755 */ 756 @Function(schema="javatest", implementor="postgresql_xml", 757 provides="lowLevelXMLEcho") 758 public static SQLXML lowLevelXMLEcho( 759 SQLXML sx, int how, @SQLType(defaultValue={}) ResultSet adjust) 760 throws SQLException 761 { 762 Connection c = DriverManager.getConnection("jdbc:default:connection"); 763 SQLXML rx = c.createSQLXML(); 764 765 if ( null == adjust ) 766 return oldSchoolLowLevelEcho(rx, sx, how); 767 768 Adjusting.XML.SourceResult axsr = 769 rx.setResult(Adjusting.XML.SourceResult.class); 770 771 switch ( how ) 772 { 773 /* 774 * The first four cases all present the content as unparsed bytes or 775 * characters, so there is nothing to adjust on the source side. 776 */ 777 case 1: 778 axsr.set(new StreamSource(sx.getBinaryStream())); 779 break; 780 case 2: 781 axsr.set(new StreamSource(sx.getCharacterStream())); 782 break; 783 case 3: 784 axsr.set(sx.getString()); 785 break; 786 case 4: 787 axsr.set(sx.getSource(StreamSource.class)); 788 break; 789 /* 790 * The remaining cases present the content in parsed form, and therefore 791 * may involve parsers that can be adjusted according to the supplied 792 * preferences. 793 */ 794 case 5: 795 axsr.set(applyAdjustments(adjust, 796 sx.getSource(Adjusting.XML.SAXSource.class))); 797 break; 798 case 6: 799 axsr.set(applyAdjustments(adjust, 800 sx.getSource(Adjusting.XML.StAXSource.class))); 801 break; 802 case 7: 803 axsr.set(applyAdjustments(adjust, 804 sx.getSource(Adjusting.XML.DOMSource.class))); 805 break; 806 default: 807 throw new SQLDataException( 808 "how must be 1-7 for lowLevelXMLEcho", "22003"); 809 } 810 811 /* 812 * Adjustments can also be applied to the SourceResult itself, where 813 * they will affect any implicitly-created parser used to verify or 814 * re-encode the content, if it was supplied in unparsed form. 815 */ 816 return applyAdjustments(adjust, axsr).get().getSQLXML(); 817 } 818 819 /** 820 * Apply adjustments (supplied as a row type with a named column for each 821 * desired adjustment and its value) to an instance of 822 * {@link Adjusting.XML.Parsing}. 823 *<p> 824 * Column names in the <em>adjust</em> row are case-insensitive versions of 825 * the method names in {@link Adjusting.XML.Parsing}, and the value of each 826 * column should be of the appropriate type (if the method has a parameter). 827 * @param adjust A row type as described above, possibly of no columns if no 828 * adjustments are wanted 829 * @param axp An instance of Adjusting.XML.Parsing 830 * @return axp, after applying any adjustments 831 */ 832 public static <T extends Adjusting.XML.Parsing<? super T>> 833 T applyAdjustments(ResultSet adjust, T axp) 834 throws SQLException 835 { 836 ResultSetMetaData rsmd = adjust.getMetaData(); 837 int n = rsmd.getColumnCount(); 838 839 for ( int i = 1; i <= n; ++i ) 840 { 841 String k = rsmd.getColumnLabel(i); 842 if ( "lax".equalsIgnoreCase(k) ) 843 axp.lax(adjust.getBoolean(i)); 844 else if ( "allowDTD".equalsIgnoreCase(k) ) 845 axp.allowDTD(adjust.getBoolean(i)); 846 else if ( "ignoreDTD".equalsIgnoreCase(k) ) 847 axp.ignoreDTD(); 848 else if ( "externalGeneralEntities".equalsIgnoreCase(k) ) 849 axp.externalGeneralEntities(adjust.getBoolean(i)); 850 else if ( "externalParameterEntities".equalsIgnoreCase(k) ) 851 axp.externalParameterEntities(adjust.getBoolean(i)); 852 else if ( "loadExternalDTD".equalsIgnoreCase(k) ) 853 axp.loadExternalDTD(adjust.getBoolean(i)); 854 else if ( "xIncludeAware".equalsIgnoreCase(k) ) 855 axp.xIncludeAware(adjust.getBoolean(i)); 856 else if ( "expandEntityReferences".equalsIgnoreCase(k) ) 857 axp.expandEntityReferences(adjust.getBoolean(i)); 858 else if ( "elementAttributeLimit".equalsIgnoreCase(k) ) 859 axp.elementAttributeLimit(adjust.getInt(i)); 860 else if ( "entityExpansionLimit".equalsIgnoreCase(k) ) 861 axp.entityExpansionLimit(adjust.getInt(i)); 862 else if ( "entityReplacementLimit".equalsIgnoreCase(k) ) 863 axp.entityReplacementLimit(adjust.getInt(i)); 864 else if ( "maxElementDepth".equalsIgnoreCase(k) ) 865 axp.maxElementDepth(adjust.getInt(i)); 866 else if ( "maxGeneralEntitySizeLimit".equalsIgnoreCase(k) ) 867 axp.maxGeneralEntitySizeLimit(adjust.getInt(i)); 868 else if ( "maxParameterEntitySizeLimit".equalsIgnoreCase(k) ) 869 axp.maxParameterEntitySizeLimit(adjust.getInt(i)); 870 else if ( "maxXMLNameLimit".equalsIgnoreCase(k) ) 871 axp.maxXMLNameLimit(adjust.getInt(i)); 872 else if ( "totalEntitySizeLimit".equalsIgnoreCase(k) ) 873 axp.totalEntitySizeLimit(adjust.getInt(i)); 874 else if ( "accessExternalDTD".equalsIgnoreCase(k) ) 875 axp.accessExternalDTD(adjust.getString(i)); 876 else if ( "accessExternalSchema".equalsIgnoreCase(k) ) 877 axp.accessExternalSchema(adjust.getString(i)); 878 else if ( "schema".equalsIgnoreCase(k) ) 879 { 880 try 881 { 882 axp.schema(s_schemas.get(adjust.getString(i))); 883 } 884 catch (UnsupportedOperationException e) 885 { 886 } 887 } 888 else 889 throw new SQLDataException( 890 "unrecognized name \"" + k + "\" for parser adjustment", 891 "22000"); 892 } 893 return axp; 894 } 895 896 /** 897 * An obsolescent example, showing what was required to copy from one 898 * {@code SQLXML} object to another, using the various supported APIs, 899 * without using {@link Adjusting.XML.SourceResult}, or at least without 900 * using it much. It is still used in case 4 to be sure of getting a 901 * {@code StreamResult} that matches the byte-or-character-ness of the 902 * {@code StreamSource}. How to handle that case without 903 * {@code SourceResult} is left as an exercise. 904 */ 905 private static SQLXML oldSchoolLowLevelEcho(SQLXML rx, SQLXML sx, int how) 906 throws SQLException 907 { 908 try 909 { 910 switch ( how ) 911 { 912 case 1: 913 InputStream is = sx.getBinaryStream(); 914 OutputStream os = rx.setBinaryStream(); 915 shovelBytes(is, os); 916 break; 917 case 2: 918 Reader r = sx.getCharacterStream(); 919 Writer w = rx.setCharacterStream(); 920 shovelChars(r, w); 921 break; 922 case 3: 923 rx.setString(sx.getString()); 924 break; 925 case 4: 926 StreamSource ss = sx.getSource(StreamSource.class); 927 Adjusting.XML.StreamResult sr = 928 rx.setResult(Adjusting.XML.StreamResult.class); 929 is = ss.getInputStream(); 930 r = ss.getReader(); 931 if ( null != is ) 932 { 933 os = sr.preferBinaryStream().get().getOutputStream(); 934 shovelBytes(is, os); 935 break; 936 } 937 if ( null != r ) 938 { 939 w = sr.preferCharacterStream().get().getWriter(); 940 shovelChars(r, w); 941 break; 942 } 943 throw new SQLDataException( 944 "StreamSource contained neither InputStream nor Reader"); 945 case 5: 946 SAXSource sxs = sx.getSource(SAXSource.class); 947 SAXResult sxr = rx.setResult(SAXResult.class); 948 XMLReader xr = sxs.getXMLReader(); 949 if ( null == xr ) 950 { 951 SAXParserFactory spf = SAXParserFactory.newInstance(); 952 spf.setNamespaceAware(true); 953 xr = spf.newSAXParser().getXMLReader(); 954 /* 955 * Important: before copying this example code for another 956 * use, consider whether the input XML might be untrusted. 957 * If so, the new XMLReader created here should have several 958 * features given safe default settings as outlined in the 959 * OWASP guidelines. (This branch is not reached when sx is 960 * a PL/Java native SQLXML instance, as xr will be non-null 961 * and already configured.) 962 */ 963 } 964 ContentHandler ch = sxr.getHandler(); 965 xr.setContentHandler(ch); 966 if ( ch instanceof DTDHandler ) 967 xr.setDTDHandler((DTDHandler)ch); 968 LexicalHandler lh = sxr.getLexicalHandler(); 969 if ( null == lh && ch instanceof LexicalHandler ) 970 lh = (LexicalHandler)ch; 971 if ( null != lh ) 972 xr.setProperty( 973 "http://xml.org/sax/properties/lexical-handler", lh); 974 xr.parse(sxs.getInputSource()); 975 break; 976 case 6: 977 StAXSource sts = sx.getSource(StAXSource.class); 978 StAXResult str = rx.setResult(StAXResult.class); 979 XMLOutputFactory xof = XMLOutputFactory.newInstance(); 980 /* 981 * The Source has either an event reader or a stream reader. Use 982 * the event reader directly, or create one around the stream 983 * reader. 984 */ 985 XMLEventReader xer = sts.getXMLEventReader(); 986 if ( null == xer ) 987 { 988 XMLInputFactory xif = XMLInputFactory .newInstance(); 989 xif.setProperty(xif.IS_NAMESPACE_AWARE, true); 990 /* 991 * Important: before copying this example code for another 992 * use, consider whether the input XML might be untrusted. 993 * If so, the new XMLInputFactory created here might want 994 * several properties given safe default settings as 995 * outlined in the OWASP guidelines. (When sx is a PL/Java 996 * native SQLXML instance, the XMLStreamReader obtained 997 * below will already have been so configured.) 998 */ 999 xer = xif.createXMLEventReader(sts.getXMLStreamReader()); 1000 } 1001 /* 1002 * Were you thinking the above could be simply 1003 * createXMLEventReader(sts) by analogy with the writer below? 1004 * Good thought, but the XMLInputFactory implementation that's 1005 * included in OpenJDK doesn't implement the case where the 1006 * Source argument is a StAXSource! Two lines would do it. 1007 */ 1008 /* 1009 * Because of a regression in Java 9 and later, the line below, 1010 * while working in Java 8 and earlier, will produce a 1011 * ClassCastException in Java 9 through (for sure) 12, (almost 1012 * certainly) 13, and on until some future version fixes the 1013 * regression, if ever, if 'str' wraps any XMLStreamWriter 1014 * implementation other than the inaccessible one from the guts 1015 * of the JDK itself. The bug has been reported but (as of this 1016 * writing) is still in the maddening limbo phase of the Java 1017 * bug reporting cycle, where no bug number can refer to it. See 1018 * lowLevelXMLEcho() above for code to do this copy successfully 1019 * using an Adjusting.XML.SourceResult. 1020 */ 1021 XMLEventWriter xew = xof.createXMLEventWriter(str); 1022 xew.add(xer); 1023 xew.close(); 1024 xer.close(); 1025 break; 1026 case 7: 1027 DOMSource ds = sx.getSource(DOMSource.class); 1028 DOMResult dr = rx.setResult(DOMResult.class); 1029 dr.setNode(ds.getNode()); 1030 break; 1031 default: 1032 throw new SQLDataException( 1033 "how must be 1-7 for lowLevelXMLEcho", "22003"); 1034 } 1035 } 1036 catch ( IOException e ) 1037 { 1038 throw new SQLException( 1039 "IOException in lowLevelXMLEcho", "58030", e); 1040 } 1041 catch ( 1042 ParserConfigurationException | SAXException | XMLStreamException e ) 1043 { 1044 throw new SQLException( 1045 "XML exception in lowLevelXMLEcho", "22000", e); 1046 } 1047 return rx; 1048 } 1049 1050 /** 1051 * Proxy a PL/Java SQLXML source object as if it were of a non-PL/Java 1052 * implementing class, to confirm that it can still be returned successfully 1053 * to PostgreSQL. 1054 * @param sx readable {@code SQLXML} object to proxy 1055 * @param how 1,2,4,5,6,7 determines what subclass of {@code Source} will be 1056 * returned by {@code getSource}. 1057 */ 1058 @Function(schema="javatest", implementor="postgresql_xml", 1059 provides="proxiedXMLEcho") 1060 public static SQLXML proxiedXMLEcho(SQLXML sx, int how) 1061 throws SQLException 1062 { 1063 return new SQLXMLProxy(sx, how); 1064 } 1065 1066 /** 1067 * Supply a sequence of bytes to be the exact (encoded) content of an XML 1068 * value, which will be returned; if the encoding is not UTF-8, the value 1069 * should begin with an XML Decl that names the encoding. 1070 *<p> 1071 * Constructs an {@code SQLXML} instance that will return the supplied 1072 * content as a {@code StreamSource} wrapping an {@code InputStream}, or via 1073 * {@code getBinaryStream}, but fail if asked for any other form. 1074 */ 1075 @Function(schema="javatest", implementor="postgresql_xml", 1076 provides="mockedXMLEchoB") 1077 public static SQLXML mockedXMLEcho(byte[] bytes) 1078 throws SQLException 1079 { 1080 return new SQLXMLMock(bytes); 1081 } 1082 1083 /** 1084 * Supply a sequence of characters to be the exact (Unicode) content of an 1085 * XML value, which will be returned; if the value begins with an XML Decl 1086 * that names an encoding, the content will be assumed to contain only 1087 * characters representable in that encoding. 1088 *<p> 1089 * Constructs an {@code SQLXML} instance that will return the supplied 1090 * content as a {@code StreamSource} wrapping a {@code Reader}, or via 1091 * {@code getCharacterStream}, but fail if asked for any other form. 1092 */ 1093 @Function(schema="javatest", implementor="postgresql_xml", 1094 provides="mockedXMLEchoC") 1095 public static SQLXML mockedXMLEcho(String chars) 1096 throws SQLException 1097 { 1098 return new SQLXMLMock(chars); 1099 } 1100 1101 /** 1102 * Text-typed variant of lowLevelXMLEcho (does not require XML type). 1103 */ 1104 @Function(schema="javatest", name="lowLevelXMLEcho", 1105 type="text") 1106 public static SQLXML lowLevelXMLEcho_(@SQLType("text") SQLXML sx, int how, 1107 @SQLType(defaultValue={}) ResultSet adjust) 1108 throws SQLException 1109 { 1110 return lowLevelXMLEcho(sx, how, adjust); 1111 } 1112 1113 /** 1114 * Low-level XML echo where the Java parameter and return type are String. 1115 */ 1116 @Function(schema="javatest", implementor="postgresql_xml", type="xml") 1117 public static String lowLevelXMLEcho(@SQLType("xml") String x) 1118 throws SQLException 1119 { 1120 return x; 1121 } 1122 1123 /** 1124 * Create some XML, pass it to a {@code SELECT ?} prepared statement, 1125 * retrieve it from the result set, and return it via the out-parameter 1126 * result set of this {@code RECORD}-returning function. 1127 */ 1128 @Function(schema="javatest", type="RECORD") 1129 public static boolean xmlInStmtAndRS(ResultSet out) throws SQLException 1130 { 1131 Connection c = DriverManager.getConnection("jdbc:default:connection"); 1132 SQLXML x = c.createSQLXML(); 1133 x.setString("<a/>"); 1134 PreparedStatement ps = c.prepareStatement("SELECT ?"); 1135 ps.setObject(1, x, Types.SQLXML); 1136 ResultSet rs = ps.executeQuery(); 1137 rs.next(); 1138 if ( Types.SQLXML != rs.getMetaData().getColumnType(1) ) 1139 logMessage("WARNING", 1140 "ResultSetMetaData.getColumnType() misreports SQLXML"); 1141 x = rs.getSQLXML(1); 1142 ps.close(); 1143 out.updateObject(1, x); 1144 return true; 1145 } 1146 1147 /** 1148 * Test serialization into the PostgreSQL server encoding by returning 1149 * a text node, optionally wrapped in an element, containing the supplied 1150 * stuff. 1151 *<p> 1152 * The stuff is supplied as a {@code bytea} and a named <em>encoding</em>, 1153 * so it is easy to supply stuff that isn't in the server encoding and see 1154 * what the serializer does with it. 1155 *<p> 1156 * As of this writing, if the <em>stuff</em>, decoded according to 1157 * <em>encoding</em>, contains characters that are not representable in the 1158 * server encoding, the serializers supplied in the JRE will: 1159 *<ul> 1160 *<li>SAX, DOM: replace the character with a numeric character reference if 1161 * the node is wrapped in an element, but not outside of an element; there, 1162 * PL/Java ensures an {@code UnmappableCharacterException} is thrown, as the 1163 * serializer would otherwise silently lose information by replacing the 1164 * character with a {@code ?}. 1165 *<li>StAX: replace the character with a numeric character reference whether 1166 * wrapped in an element or not (outside of an element, this officially 1167 * violates the letter of the XML spec, but does not lose information, and 1168 * is closer to the spirit of SQL/XML with its {@code XML(CONTENT)} type). 1169 *</ul> 1170 * @param stuff Content to be used in the text node 1171 * @param encoding Name of an encoding; stuff will be decoded to Unicode 1172 * according to this encoding, and then serialized into the server encoding, 1173 * where possible. 1174 * @param how Integer specifying which XML API to test, like every other how 1175 * in this class; here the only valid choices are 5 (SAX), 6 (StAX), or 1176 * 7 (DOM). 1177 * @param inElement True if the text node should be wrapped in an element. 1178 * @return The resulting XML content. 1179 */ 1180 @Function(schema="javatest", implementor="postgresql_xml") 1181 public static SQLXML xmlTextNode( 1182 byte[] stuff, String encoding, int how, boolean inElement) 1183 throws Exception 1184 { 1185 if ( 5 > how || how > 7 ) 1186 throw new SQLDataException( 1187 "how must be 5-7 for xmlTextNode", "22003"); 1188 1189 String stuffString = new String(stuff, encoding); 1190 Connection c = DriverManager.getConnection("jdbc:default:connection"); 1191 SQLXML rx = c.createSQLXML(); 1192 1193 switch ( how ) 1194 { 1195 case 5: 1196 SAXResult sxr = rx.setResult(SAXResult.class); 1197 sxr.getHandler().startDocument(); 1198 if ( inElement ) 1199 sxr.getHandler().startElement("", "sax", "sax", 1200 new AttributesImpl()); 1201 sxr.getHandler().characters( 1202 stuffString.toCharArray(), 0, stuffString.length()); 1203 if ( inElement ) 1204 sxr.getHandler().endElement("", "sax", "sax"); 1205 sxr.getHandler().endDocument(); 1206 break; 1207 case 6: 1208 StAXResult stxr = rx.setResult(StAXResult.class); 1209 stxr.getXMLStreamWriter().writeStartDocument(); 1210 if ( inElement ) 1211 stxr.getXMLStreamWriter().writeStartElement("", "stax", ""); 1212 stxr.getXMLStreamWriter().writeCharacters(stuffString); 1213 if ( inElement ) 1214 stxr.getXMLStreamWriter().writeEndElement(); 1215 stxr.getXMLStreamWriter().writeEndDocument(); 1216 break; 1217 case 7: 1218 DOMResult dr = rx.setResult(DOMResult.class); 1219 /* 1220 * Why request features XML and Traversal? 1221 * If the only features requested are from the set 1222 * {Core, XML, LS} and maybe XPath, you get a brain-damaged 1223 * DOMImplementation that violates the org.w3c.dom.DOMImplementation 1224 * contract, as createDocument still tries to make a document 1225 * element even when passed null,null,null when, according to the 1226 * contract, it should not. To get the real implementation that 1227 * works, ask for some feature it supports outside of that core set. 1228 * I don't really need Traversal, but by asking for it, I get what 1229 * I do need. 1230 */ 1231 Document d = DOMImplementationRegistry.newInstance() 1232 .getDOMImplementation("XML Traversal") 1233 .createDocument(null, null, null); 1234 DocumentFragment df = d.createDocumentFragment(); 1235 ( inElement ? df.appendChild(d.createElement("dom")) : df ) 1236 .appendChild(d.createTextNode(stuffString)); 1237 dr.setNode(df); 1238 break; 1239 } 1240 return rx; 1241 } 1242 1243 /** 1244 * Create and leave some number of SQLXML objects unclosed, unused, and 1245 * unreferenced, as a test of reclamation. 1246 * @param howmany Number of SQLXML instances to create. 1247 * @param how If nonzero, the flavor of writing to request on the object 1248 * before abandoning it; if zero, it is left in its initial, writable state. 1249 */ 1250 @Function(schema="javatest") 1251 public static void unclosedSQLXML(int howmany, int how) throws SQLException 1252 { 1253 Connection c = DriverManager.getConnection("jdbc:default:connection"); 1254 while ( howmany --> 0 ) 1255 { 1256 SQLXML sx = c.createSQLXML(); 1257 if ( 0 < how ) 1258 sxToResult(sx, how); 1259 } 1260 } 1261 1262 1263 /** 1264 * Return some instance of {@code Source} for reading an {@code SQLXML} 1265 * object, depending on the parameter {@code how}. 1266 *<p> 1267 * Note that this method always returns a {@code Source}, even for cases 1268 * 1 and 2 (obtaining readable streams directly from the {@code SQLXML} 1269 * object; this method wraps them in {@code Source}), and case 3 1270 * ({@code getString}; this method creates a {@code StringReader} and 1271 * returns it wrapped in a {@code Source}. 1272 */ 1273 private static Source sxToSource(SQLXML sx, int how) throws SQLException 1274 { 1275 switch ( how ) 1276 { 1277 case 1: return new StreamSource(sx.getBinaryStream()); 1278 case 2: return new StreamSource(sx.getCharacterStream()); 1279 case 3: return new StreamSource(new StringReader(sx.getString())); 1280 case 4: return sx.getSource(StreamSource.class); 1281 case 5: return sx.getSource(SAXSource.class); 1282 case 6: return sx.getSource(StAXSource.class); 1283 case 7: return sx.getSource(DOMSource.class); 1284 default: throw new SQLDataException("how should be 1-7", "22003"); 1285 } 1286 } 1287 1288 /** 1289 * Return some instance of {@code Result} for writing an {@code SQLXML} 1290 * object, depending on the parameter {@code how}. 1291 *<p> 1292 * Note that this method always returns a {@code Result}, even for cases 1293 * 1 and 2 (obtaining writable streams directly from the {@code SQLXML} 1294 * object; this method wraps them in {@code Result}), and case 3 1295 * ({@code setString}; this method creates a {@code StringWriter} and 1296 * returns it wrapped in a {@code Result}. 1297 *<p> 1298 * In case 3, it will be necessary, after writing, to get the {@code String} 1299 * from the {@code StringWriter}, and call {@code setString} with it. 1300 */ 1301 private static Result sxToResult(SQLXML sx, int how) throws SQLException 1302 { 1303 switch ( how ) 1304 { 1305 case 1: return new StreamResult(sx.setBinaryStream()); 1306 case 2: return new StreamResult(sx.setCharacterStream()); 1307 case 3: return new StreamResult(new StringWriter()); 1308 case 4: return sx.setResult(StreamResult.class); 1309 case 5: return sx.setResult(SAXResult.class); 1310 case 6: return sx.setResult(StAXResult.class); 1311 case 7: 1312 DOMResult r = sx.setResult(DOMResult.class); 1313 allowFragment(r); // else it'll accept only DOCUMENT form 1314 return r; 1315 default: throw new SQLDataException("how should be 1-7", "22003"); 1316 } 1317 } 1318 1319 /** 1320 * Return some instance of {@code Source} for reading an {@code SQLXML} 1321 * object, depending on the parameter {@code how}, applying any adjustments 1322 * in {@code adjust}. 1323 *<p> 1324 * Allows {@code how} to be zero, meaning to let the implementation choose 1325 * what kind of {@code Source} to present. Otherwise identical to the other 1326 * {@code sxToSource}. 1327 */ 1328 private static Source sxToSource(SQLXML sx, int how, ResultSet adjust) 1329 throws SQLException 1330 { 1331 Source s; 1332 switch ( how ) 1333 { 1334 case 0: s = sx.getSource(Adjusting.XML.Source.class); break; 1335 case 1: 1336 case 2: 1337 case 3: 1338 case 4: 1339 return sxToSource(sx, how); // no adjustments on a StreamSource 1340 case 5: s = sx.getSource(Adjusting.XML.SAXSource.class); break; 1341 case 6: s = sx.getSource(Adjusting.XML.StAXSource.class); break; 1342 case 7: s = sx.getSource(Adjusting.XML.DOMSource.class); break; 1343 default: throw new SQLDataException("how should be 0-7", "22003"); 1344 } 1345 1346 if ( s instanceof Adjusting.XML.Source ) 1347 return applyAdjustments(adjust, (Adjusting.XML.Source<?>)s).get(); 1348 return s; 1349 } 1350 1351 /** 1352 * Return some instance of {@code Result} for writing an {@code SQLXML} 1353 * object, depending on the parameter {@code how} applying any adjustments 1354 * in {@code adjust}. 1355 *<p> 1356 * Allows {@code how} to be zero, meaning to let the implementation choose 1357 * what kind of {@code Result} to present. Otherwise identical to the other 1358 * {@code sxToResult}. 1359 */ 1360 private static Result sxToResult(SQLXML sx, int how, ResultSet adjust) 1361 throws SQLException 1362 { 1363 Result r; 1364 switch ( how ) 1365 { 1366 case 1: // you might wish you could adjust a raw BinaryStream 1367 case 2: // or CharacterStream 1368 case 3: // or String, but you can't. Ask for a StreamResult. 1369 case 5: // SAXResult needs no adjustment 1370 case 6: // StAXResult needs no adjustment 1371 case 7: // DOMResult needs no adjustment 1372 return sxToResult(sx, how); 1373 case 4: r = sx.setResult(Adjusting.XML.StreamResult.class); break; 1374 case 0: r = sx.setResult(Adjusting.XML.Result.class); break; 1375 default: throw new SQLDataException("how should be 0-7", "22003"); 1376 } 1377 1378 if ( r instanceof Adjusting.XML.Result ) 1379 return applyAdjustments(adjust, (Adjusting.XML.Result<?>)r).get(); 1380 return r; 1381 } 1382 1383 /** 1384 * Ensure the closing of whatever method was used to add content to 1385 * an {@code SQLXML} object. 1386 *<p> 1387 * Before a {@code SQLXML} object that has been written to can be used by 1388 * PostgreSQL (returned as a function result, plugged in as a prepared 1389 * statement parameter or into a {@code ResultSet}, etc.), the method used 1390 * for writing it must be "closed" to ensure the writing is complete. 1391 *<p> 1392 * If it is set with {@link SQLXML#setString setString}, nothing more is 1393 * needed; {@code setString} obviously sets the whole value at once. Any 1394 * {@code OutputStream} or {@code Writer} obtained from 1395 * {@link SQLXML#setBinaryStream setBinaryStream} or 1396 * {@link SQLXML#setCharacterStream setCharacterStream}, or from 1397 * {@link SQLXML#setResult setResult}{@code (StreamResult.class)}, has to be 1398 * explicitly closed (a {@link Transformer} does not close its 1399 * {@link Result} when the transformation is complete!). 1400 * Those are cases 1, 2, and 4 here. 1401 *<p> 1402 * Cases 5 ({@code SAXResult}) and 6 ({@code StAXResult}) need no special 1403 * attention; though the {@code Transformer} does not close them, the ones 1404 * returned by this {@code SQLXML} implementation are set up to close 1405 * themselves when the {@code endDocument} event is written. 1406 *<p> 1407 * Case 3 (test of {@code setString} is handled specially here. As this 1408 * class allows testing of all techniques for writing the {@code SQLXML} 1409 * object, and most of those involve a {@code Result}, case 3 is handled 1410 * by also constructing a {@code Result} over a {@link StringWriter} and 1411 * having the content written into that; this method then extracts the 1412 * content from the {@code StringWriter} and passes it to {@code setString}. 1413 * For cases 1 and 2, likewise, the stream obtained with 1414 * {@code getBinaryStream} or {@code getCharacterStream} has been wrapped in 1415 * a {@code Result} for generality in this example. 1416 *<p> 1417 * A typical application will not need the generality seen here; it 1418 * will usually know which technique it is using to write the {@code SQLXML} 1419 * object, and only needs to know how to close that if it needs closing. 1420 * @param r The {@code Result} onto which writing was done. 1421 * @param sx The {@code SQLXML} object being written. 1422 * @param how The integer used in this example class to select which method 1423 * of writing the {@code SQLXML} object was to be tested. 1424 * @return The {@code SQLXML} object {@code sx}, because why not? 1425 */ 1426 public static SQLXML ensureClosed(Result r, SQLXML sx, int how) 1427 throws SQLException 1428 { 1429 switch ( how ) 1430 { 1431 case 1: 1432 case 2: 1433 case 4: 1434 StreamResult sr = (StreamResult)r; 1435 OutputStream os = sr.getOutputStream(); 1436 Writer w = sr.getWriter(); 1437 try 1438 { 1439 if ( null != os ) 1440 os.close(); 1441 if ( null != w ) 1442 w.close(); 1443 } 1444 catch ( IOException ioe ) 1445 { 1446 throw new SQLException( 1447 "Failure closing SQLXML result", "XX000"); 1448 } 1449 break; 1450 case 3: 1451 StringWriter sw = (StringWriter)((StreamResult)r).getWriter(); 1452 String s = sw.toString(); 1453 sx.setString(s); 1454 break; 1455 } 1456 return sx; 1457 } 1458 1459 /** 1460 * Configure a {@code DOMResult} to accept {@code CONTENT} (a/k/a 1461 * document fragment), not only the more restrictive {@code DOCUMENT}. 1462 *<p> 1463 * The other forms of {@code Result} that can be requested will happily 1464 * accept {@code XML(CONTENT)} and not just {@code XML(DOCUMENT)}. 1465 * The {@code DOMResult} is pickier, however: if you first call 1466 * {@link DOMResult#setNode setNode} with a {@code DocumentFragment}, it 1467 * will accept either form, but if you leave the node unset when passing the 1468 * {@code DOMResult} to a transformer, the transformer will default to 1469 * putting a {@code Document} node there, and then it will not accept a 1470 * fragment. 1471 *<p> 1472 * If you need to handle fragments, this method illustrates how to pre-load 1473 * the {@code DOMResult} with an empty {@code DocumentFragment}. Note that 1474 * if you use some XML processing package that supplies its own classes 1475 * implementing DOM nodes, you may need to use a {@code DocumentFragment} 1476 * instance obtained from that package. 1477 */ 1478 public static void allowFragment(DOMResult r) throws SQLException 1479 { 1480 try 1481 { 1482 r.setNode(DocumentBuilderFactory.newInstance() 1483 .newDocumentBuilder().newDocument() 1484 .createDocumentFragment()); 1485 } 1486 catch ( ParserConfigurationException pce ) 1487 { 1488 throw new SQLException("Failed initializing DOMResult", pce); 1489 } 1490 } 1491 1492 private static void shovelBytes(InputStream is, OutputStream os) 1493 throws IOException 1494 { 1495 byte[] b = new byte[8192]; 1496 int got; 1497 while ( -1 != (got = is.read(b)) ) 1498 os.write(b, 0, got); 1499 is.close(); 1500 os.close(); 1501 } 1502 1503 private static void shovelChars(Reader r, Writer w) 1504 throws IOException 1505 { 1506 char[] b = new char[8192]; 1507 int got; 1508 while ( -1 != (got = r.read(b)) ) 1509 w.write(b, 0, got); 1510 r.close(); 1511 w.close(); 1512 } 1513 1514 /** 1515 * Test the MappedUDT (in one direction anyway). 1516 *<p> 1517 * Creates a {@code PassXML} object, the Java class that maps the 1518 * {@code javatest.onexml} composite type, which has one member, of XML 1519 * type. Stores a {@code SQLXML} value in that field of the {@code PassXML} 1520 * object, and passes that to an SQL query that expects and returns 1521 * {@code javatest.onexml}. Retrieves the XML from the value field of the 1522 * {@code PassXML} object created to map the result of the query. 1523 * @return The original XML value, if all goes well. 1524 */ 1525 @Function(schema="javatest", implementor="postgresql_xml") 1526 public static SQLXML xmlFromComposite() throws SQLException 1527 { 1528 Connection c = DriverManager.getConnection("jdbc:default:connection"); 1529 PreparedStatement ps = 1530 c.prepareStatement("SELECT CAST(? AS javatest.onexml)"); 1531 SQLXML x = c.createSQLXML(); 1532 x.setString("<a/>"); 1533 PassXML obj = new PassXML(); 1534 obj.m_value = x; 1535 obj.m_typeName = "javatest.onexml"; 1536 ps.setObject(1, obj); 1537 ResultSet r = ps.executeQuery(); 1538 r.next(); 1539 obj = (PassXML)r.getObject(1); 1540 ps.close(); 1541 return obj.m_value; 1542 } 1543 1544 /* 1545 * Required to serve as a MappedUDT: 1546 */ 1547 /** 1548 * No-arg constructor required of objects that will implement 1549 * {@link SQLData}. 1550 */ 1551 public PassXML() { } 1552 1553 private String m_typeName; 1554 private SQLXML m_value; 1555 1556 @Override 1557 public String getSQLTypeName() { return m_typeName; } 1558 1559 @Override 1560 public void readSQL(SQLInput stream, String typeName) throws SQLException 1561 { 1562 m_typeName = typeName; 1563 m_value = (SQLXML) stream.readObject(); 1564 } 1565 1566 @Override 1567 public void writeSQL(SQLOutput stream) throws SQLException 1568 { 1569 stream.writeSQLXML(m_value); 1570 } 1571 1572 /** 1573 * Class that will proxy methods to another {@code SQLXML} class. 1574 *<p> 1575 * Used for testing the PL/Java can accept input for PostgreSQL from an 1576 * {@code SQLXML} object not of its own implementation (for example, one 1577 * obtained from a different JDBC driver from some other database). 1578 *<p> 1579 * Only the {@code getSource} method is specially treated, to allow 1580 * exercising the various flavors of source. 1581 */ 1582 public static class SQLXMLProxy implements SQLXML 1583 { 1584 private SQLXML m_sx; 1585 private int m_how; 1586 1587 public SQLXMLProxy(SQLXML sx, int how) 1588 { 1589 if ( null == sx ) 1590 throw new NullPointerException("Null SQLXMLProxy target"); 1591 if ( 1 > how || how > 7 || how == 3 ) 1592 throw new IllegalArgumentException( 1593 "\"how\" must be 1, 2, 4, 5, 6, or 7"); 1594 m_sx = sx; 1595 m_how = how; 1596 } 1597 1598 @Override 1599 public void free() throws SQLException { m_sx.free(); } 1600 1601 @Override 1602 public InputStream getBinaryStream() throws SQLException 1603 { 1604 return m_sx.getBinaryStream(); 1605 } 1606 1607 @Override 1608 public OutputStream setBinaryStream() throws SQLException 1609 { 1610 return m_sx.setBinaryStream(); 1611 } 1612 1613 @Override 1614 public Reader getCharacterStream() throws SQLException 1615 { 1616 return m_sx.getCharacterStream(); 1617 } 1618 1619 @Override 1620 public Writer setCharacterStream() throws SQLException 1621 { 1622 return m_sx.setCharacterStream(); 1623 } 1624 1625 @Override 1626 public String getString() throws SQLException 1627 { 1628 return m_sx.getString(); 1629 } 1630 1631 @Override 1632 public void setString(String value) throws SQLException 1633 { 1634 m_sx.setString(value); 1635 } 1636 1637 @Override 1638 @SuppressWarnings("unchecked") // all the fun's when sourceClass is null 1639 public <T extends Source> T getSource(Class<T> sourceClass) 1640 throws SQLException 1641 { 1642 if ( null == sourceClass ) 1643 { 1644 switch ( m_how ) 1645 { 1646 case 1: 1647 return (T)new StreamSource(m_sx.getBinaryStream()); 1648 case 2: 1649 return (T)new StreamSource(m_sx.getCharacterStream()); 1650 case 4: 1651 sourceClass = (Class<T>)StreamSource.class; 1652 break; 1653 case 5: 1654 sourceClass = (Class<T>)SAXSource.class; 1655 break; 1656 case 6: 1657 sourceClass = (Class<T>)StAXSource.class; 1658 break; 1659 case 7: 1660 sourceClass = (Class<T>)DOMSource.class; 1661 break; 1662 } 1663 } 1664 return m_sx.getSource(sourceClass); 1665 } 1666 1667 @Override 1668 public <T extends Result> T setResult(Class<T> resultClass) 1669 throws SQLException 1670 { 1671 return m_sx.setResult(resultClass); 1672 } 1673 } 1674 1675 /** 1676 * Class that will mock an {@code SQLXML} instance, returning only binary or 1677 * character stream data from a byte array or string supplied at 1678 * construction. 1679 */ 1680 public static class SQLXMLMock implements SQLXML 1681 { 1682 private String m_chars; 1683 private byte[] m_bytes; 1684 1685 public SQLXMLMock(String content) 1686 { 1687 if ( null == content ) 1688 throw new NullPointerException("Null SQLXMLMock content"); 1689 m_chars = content; 1690 } 1691 1692 public SQLXMLMock(byte[] content) 1693 { 1694 if ( null == content ) 1695 throw new NullPointerException("Null SQLXMLMock content"); 1696 m_bytes = content; 1697 } 1698 1699 @Override 1700 public void free() throws SQLException { } 1701 1702 @Override 1703 public InputStream getBinaryStream() throws SQLException 1704 { 1705 if ( null != m_bytes ) 1706 return new ByteArrayInputStream(m_bytes); 1707 throw new UnsupportedOperationException( 1708 "SQLXMLMock.getBinaryStream"); 1709 } 1710 1711 @Override 1712 public OutputStream setBinaryStream() throws SQLException 1713 { 1714 throw new UnsupportedOperationException( 1715 "SQLXMLMock.setBinaryStream"); 1716 } 1717 1718 @Override 1719 public Reader getCharacterStream() throws SQLException 1720 { 1721 if ( null != m_chars ) 1722 return new StringReader(m_chars); 1723 throw new UnsupportedOperationException( 1724 "SQLXMLMock.getCharacterStream"); 1725 } 1726 1727 @Override 1728 public Writer setCharacterStream() throws SQLException 1729 { 1730 throw new UnsupportedOperationException( 1731 "SQLXMLMock.setCharacterStream"); 1732 } 1733 1734 @Override 1735 public String getString() throws SQLException 1736 { 1737 if ( null != m_chars ) 1738 return m_chars; 1739 throw new UnsupportedOperationException( 1740 "SQLXMLMock.getString"); 1741 } 1742 1743 @Override 1744 public void setString(String value) throws SQLException 1745 { 1746 throw new UnsupportedOperationException( 1747 "SQLXMLMock.setString"); 1748 } 1749 1750 @Override 1751 @SuppressWarnings("unchecked") // sourceClass==StreamSource is verified 1752 public <T extends Source> T getSource(Class<T> sourceClass) 1753 throws SQLException 1754 { 1755 if ( null != sourceClass && StreamSource.class != sourceClass ) 1756 throw new UnsupportedOperationException( 1757 "SQLXMLMock.getSource(" + sourceClass.getName() + ")"); 1758 if ( null != m_chars ) 1759 return (T) new StreamSource(new StringReader(m_chars)); 1760 return (T) new StreamSource(new ByteArrayInputStream(m_bytes)); 1761 } 1762 1763 @Override 1764 public <T extends Result> T setResult(Class<T> resultClass) 1765 throws SQLException 1766 { 1767 throw new UnsupportedOperationException( 1768 "SQLXMLMock.setResult"); 1769 } 1770 } 1771}