From 98449454564d0230fc51f753ad89b92a19593785 Mon Sep 17 00:00:00 2001 From: satyapadala Date: Sun, 12 Apr 2026 21:35:51 +0530 Subject: [PATCH 1/2] Handle BI Publisher wrapped XML results --- .../OracleCloudSQLQuery.catalog | Bin 4200 -> 4564 bytes .../oraclefusionhub/jdbc/OFHConnection.java | 27 +- .../com/oraclefusionhub/jdbc/OFHDriver.java | 7 +- .../oraclefusionhub/jdbc/OFHResultSet.java | 183 ++++++-- .../oraclefusionhub/jdbc/OFHStatement.java | 418 +++++++++++++++--- .../oraclefusionhub/jdbc/OFHDriverTest.java | 99 ++++- .../jdbc/OFHResultSetTest.java | 69 ++- .../__files/E2ETestEmptyWrappedResult.xml | 11 + src/test/resources/__files/E2ETestFault.xml | 13 + .../__files/E2ETestWrappedXmlResult.xml | 11 + .../resources/__files/E2ETestXmlResult.xml | 11 + 11 files changed, 741 insertions(+), 108 deletions(-) create mode 100644 src/test/resources/__files/E2ETestEmptyWrappedResult.xml create mode 100644 src/test/resources/__files/E2ETestFault.xml create mode 100644 src/test/resources/__files/E2ETestWrappedXmlResult.xml create mode 100644 src/test/resources/__files/E2ETestXmlResult.xml diff --git a/out/artifacts/OracleBICatalog/OracleCloudSQLQuery.catalog b/out/artifacts/OracleBICatalog/OracleCloudSQLQuery.catalog index 340643a841f3b831806c4fb2475cd3e336c1ba68..9e0ac70151f3be43744e2af7696c0764d9e3c232 100644 GIT binary patch literal 4564 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 literal 4200 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 iterator; - private CSVRecord record = null; - private final CSVRecord header; + private final List header; + private final List> rows; + private final Map columnIndexByName; + private int currentRowIndex = -1; + private List record = null; + private boolean closed = false; + private boolean lastWasNull = false; public OFHResultSet(Iterable s) { - this.iterator = s.iterator(); - this.header = this.iterator.next(); + Iterator iterator = s.iterator(); + this.header = new ArrayList<>(); + this.rows = new ArrayList<>(); + if (iterator.hasNext()) { + CSVRecord headerRecord = iterator.next(); + for (String value : headerRecord) { + this.header.add(value); + } + + while (iterator.hasNext()) { + CSVRecord csvRecord = iterator.next(); + List row = new ArrayList<>(); + for (String value : csvRecord) { + row.add(value); + } + this.rows.add(row); + } + } + + this.columnIndexByName = createColumnIndex(this.header); + } + + public OFHResultSet(List header, List> rows) { + this.header = new ArrayList<>(header); + this.rows = new ArrayList<>(); + for (List row : rows) { + this.rows.add(new ArrayList<>(row)); + } + this.columnIndexByName = createColumnIndex(this.header); } @Override public boolean next() throws SQLException { - boolean retVal = iterator.hasNext(); - - if (iterator.hasNext()) { - //record = List.of(iterator.next().split(FIELD_SEPARATOR, -1)); - record = iterator.next(); + ensureOpen(); + if (currentRowIndex + 1 < rows.size()) { + currentRowIndex++; + record = rows.get(currentRowIndex); + return true; } - return retVal; + record = null; + currentRowIndex = rows.size(); + lastWasNull = false; + return false; } @Override public void close() throws SQLException { + closed = true; + record = null; } @Override public boolean wasNull() throws SQLException { - return false; + ensureOpen(); + return lastWasNull; } @Override public String getString(int i) throws SQLException { - return record.get(i-1); + return getValue(i); } @Override @@ -134,7 +172,7 @@ public InputStream getBinaryStream(int i) throws SQLException { @Override public String getString(String s) throws SQLException { - return null; + return getString(findColumn(s)); } @Override @@ -229,6 +267,7 @@ public String getCursorName() throws SQLException { @Override public ResultSetMetaData getMetaData() throws SQLException { + ensureOpen(); return new ResultSetMetaData() { @Override public int getColumnCount() throws SQLException { @@ -272,6 +311,7 @@ public int getColumnDisplaySize(int column) throws SQLException { @Override public String getColumnLabel(int column) throws SQLException { + validateColumnIndex(column); return header.get(column - 1); } @@ -307,12 +347,14 @@ public String getCatalogName(int column) throws SQLException { @Override public int getColumnType(int column) throws SQLException { - return 0; + validateColumnIndex(column); + return Types.VARCHAR; } @Override public String getColumnTypeName(int column) throws SQLException { - return null; + validateColumnIndex(column); + return "VARCHAR"; } @Override @@ -332,7 +374,8 @@ public boolean isDefinitelyWritable(int column) throws SQLException { @Override public String getColumnClassName(int column) throws SQLException { - return null; + validateColumnIndex(column); + return String.class.getName(); } @Override @@ -349,17 +392,22 @@ public boolean isWrapperFor(Class iface) throws SQLException { @Override public Object getObject(int i) throws SQLException { - return null; + return getString(i); } @Override public Object getObject(String s) throws SQLException { - return null; + return getString(s); } @Override public int findColumn(String s) throws SQLException { - return 2; + ensureOpen(); + Integer index = columnIndexByName.get(s); + if (index == null) { + throw new SQLException("Column not found: " + s); + } + return index + 1; } @Override @@ -384,47 +432,76 @@ public BigDecimal getBigDecimal(String s) throws SQLException { @Override public boolean isBeforeFirst() throws SQLException { - return false; + ensureOpen(); + return currentRowIndex < 0 && !rows.isEmpty(); } @Override public boolean isAfterLast() throws SQLException { - return false; + ensureOpen(); + return currentRowIndex >= rows.size() && !rows.isEmpty(); } @Override public boolean isFirst() throws SQLException { - return false; + ensureOpen(); + return currentRowIndex == 0 && record != null; } @Override public boolean isLast() throws SQLException { - return false; + ensureOpen(); + return currentRowIndex == rows.size() - 1 && record != null; } @Override public void beforeFirst() throws SQLException { - + ensureOpen(); + currentRowIndex = -1; + record = null; + lastWasNull = false; } @Override public void afterLast() throws SQLException { - + ensureOpen(); + currentRowIndex = rows.size(); + record = null; + lastWasNull = false; } @Override public boolean first() throws SQLException { - return false; + ensureOpen(); + if (rows.isEmpty()) { + record = null; + currentRowIndex = -1; + return false; + } + currentRowIndex = 0; + record = rows.get(currentRowIndex); + lastWasNull = false; + return true; } @Override public boolean last() throws SQLException { - return false; + ensureOpen(); + if (rows.isEmpty()) { + record = null; + currentRowIndex = -1; + return false; + } + currentRowIndex = rows.size() - 1; + record = rows.get(currentRowIndex); + lastWasNull = false; + return true; } @Override public int getRow() throws SQLException { - return 0; + ensureOpen(); + return record == null ? 0 : currentRowIndex + 1; } @Override @@ -442,6 +519,45 @@ public boolean previous() throws SQLException { return false; } + private Map createColumnIndex(List columns) { + Map indexByName = new LinkedHashMap<>(); + for (int i = 0; i < columns.size(); i++) { + indexByName.put(columns.get(i), i); + } + return indexByName; + } + + private String getValue(int columnIndex) throws SQLException { + ensureOpen(); + if (record == null) { + throw new SQLException("Cursor not positioned on a row"); + } + + validateColumnIndex(columnIndex); + int zeroBasedIndex = columnIndex - 1; + + if (zeroBasedIndex >= record.size()) { + lastWasNull = true; + return null; + } + + String value = record.get(zeroBasedIndex); + lastWasNull = value == null; + return value; + } + + private void validateColumnIndex(int columnIndex) throws SQLException { + if (columnIndex < 1 || columnIndex > header.size()) { + throw new SQLException("Invalid column index: " + columnIndex); + } + } + + private void ensureOpen() throws SQLException { + if (closed) { + throw new SQLException("ResultSet is closed"); + } + } + @Override public void setFetchDirection(int i) throws SQLException { @@ -869,13 +985,12 @@ public void updateRowId(String s, RowId rowId) throws SQLException { @Override public int getHoldability() throws SQLException { - return 0; + return ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override public boolean isClosed() throws SQLException { - System.out.println("isClosed::"); - return false; + return closed; } @Override @@ -1090,12 +1205,12 @@ public void updateNClob(String s, Reader reader) throws SQLException { @Override public T getObject(int i, Class aClass) throws SQLException { - return null; + return aClass.cast(getObject(i)); } @Override public T getObject(String s, Class aClass) throws SQLException { - return null; + return aClass.cast(getObject(s)); } @Override diff --git a/src/main/java/com/oraclefusionhub/jdbc/OFHStatement.java b/src/main/java/com/oraclefusionhub/jdbc/OFHStatement.java index 734b4a0..67d9046 100644 --- a/src/main/java/com/oraclefusionhub/jdbc/OFHStatement.java +++ b/src/main/java/com/oraclefusionhub/jdbc/OFHStatement.java @@ -15,21 +15,30 @@ import java.nio.charset.StandardCharsets; import java.sql.*; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Base64; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.zip.GZIPInputStream; import org.apache.commons.csv.CSVRecord; public class OFHStatement implements Statement { private final HttpURLConnection httpURLConnection; private final String payload; + private final boolean debugEnabled; private ResultSet rs; private final String reportPath; + private int maxRows = 0; - OFHStatement(HttpURLConnection connection, String reportPath) { + OFHStatement(HttpURLConnection connection, String reportPath, boolean debugEnabled) { this.httpURLConnection = connection; this.reportPath = reportPath; + this.debugEnabled = debugEnabled; this.payload = "\n" + " \n" + @@ -55,6 +64,7 @@ public class OFHStatement implements Statement { @Override public ResultSet executeQuery(String s) throws SQLException { + debug("OFHStatement.executeQuery SQL: " + s); String query = encodeXML(s); Object[] params = new Object[] { query, this.reportPath }; String finalPayload = MessageFormat.format(this.payload, params); @@ -70,7 +80,7 @@ public ResultSet executeQuery(String s) throws SQLException { os.close(); responseCode = conn.getResponseCode(); } catch (IOException e) { - System.out.println("error while getting outstream: " + e.getStackTrace()); + debug("error while getting outstream: " + e.getStackTrace()); } StringBuffer response = new StringBuffer(); @@ -78,8 +88,9 @@ public ResultSet executeQuery(String s) throws SQLException { if (responseCode == HttpURLConnection.HTTP_OK) { try { getOutput(conn, response, responseCode); + debug("OFHStatement HTTP 200 response length: " + response.length()); } catch (IOException e) { - System.out.println("error while getting instream response: " + e.getStackTrace()); + debug("error while getting instream response: " + e.getStackTrace()); } } else { @@ -87,7 +98,7 @@ public ResultSet executeQuery(String s) throws SQLException { String errorResponseReason = null; try { getOutput(conn, errorResponse, responseCode); - System.out.println("Error Response: " + errorResponse.toString()); + debug("Error Response: " + errorResponse.toString()); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { @@ -111,13 +122,15 @@ public ResultSet executeQuery(String s) throws SQLException { } conn.disconnect(); - String responseCsv = null; + String responseContent = null; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new InputSource(new StringReader(response.toString()))); - responseCsv = getResponseCSVString(doc); + responseContent = getResponseContentString(doc); + } catch (RuntimeException e) { + throw new SQLException("Parsing Error: " + e.getMessage(), e); } catch (ParserConfigurationException e) { throw new SQLException("Parsing Error: " + e.getMessage()); } catch (IOException e) { @@ -126,110 +139,354 @@ public ResultSet executeQuery(String s) throws SQLException { throw new SQLException("Parsing Error: " + e.getMessage()); } - Iterable records; + return buildResultSet(responseContent); + } + + private String getResponseContentString(Document doc) { + String base64Content = getContentDataFromDoc(doc); + debug("OFHStatement reportBytes base64 length: " + base64Content.length()); + + byte[] decodedBytes = Base64.getDecoder().decode(base64Content); + debug("OFHStatement decoded payload bytes: " + decodedBytes.length); + + String decodedContent = new String(decodedBytes, StandardCharsets.UTF_8); + debug("OFHStatement decoded payload preview: " + preview(decodedContent)); + return decodedContent; + } + + private ResultSet buildResultSet(String responseContent) throws SQLException { + String trimmedContent = stripBom(responseContent == null ? "" : responseContent).trim(); + debug("OFHStatement trimmed payload preview: " + preview(trimmedContent)); + if (trimmedContent.startsWith("<")) { + return buildXmlPayloadResultSet(trimmedContent); + } + + return buildCsvResultSet(responseContent); + } + + private ResultSet buildXmlPayloadResultSet(String xmlContent) throws SQLException { try { - records = CSVFormat.DEFAULT.parse(new StringReader(responseCsv)); + String embeddedRowset = extractEmbeddedRowsetFromPayload(xmlContent); + if (embeddedRowset != null) { + debug("OFHStatement parser branch: embedded XML ROWSET"); + return buildXmlResultSet(parseXmlDocument(embeddedRowset)); + } + + Document xmlResult = parseXmlDocument(xmlContent); + Node rootNode = xmlResult.getDocumentElement(); + String rootName = rootNode == null ? "null" : rootNode.getNodeName(); + debug("OFHStatement payload XML root: " + rootName); + + if (matchesNodeName(rootNode, "ROWSET")) { + debug("OFHStatement parser branch: XML ROWSET"); + return buildXmlResultSet(xmlResult); + } + + if (matchesNodeName(rootNode, "DATA_DS")) { + embeddedRowset = extractEmbeddedRowset(rootNode); + if (embeddedRowset != null) { + debug("OFHStatement parser branch: embedded XML ROWSET"); + return buildXmlResultSet(parseXmlDocument(embeddedRowset)); + } + + if (isEmptyDataSet(rootNode)) { + debug("OFHStatement parser branch: empty DATA_DS result"); + return emptyResultSet(); + } + } + + throw new SQLException("Unsupported XML payload root: " + rootName); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new SQLException("XML Result Parsing Error: " + e.getMessage(), e); + } + } + + private String extractEmbeddedRowsetFromPayload(String xmlContent) { + String normalizedContent = stripBom(xmlContent); + if (normalizedContent == null || !normalizedContent.contains("")) { + return null; + } + + int resultStart = normalizedContent.indexOf(""); + int resultEnd = normalizedContent.indexOf("", resultStart); + if (resultStart < 0 || resultEnd < 0) { + return null; + } + + String escapedRowset = normalizedContent.substring(resultStart + "".length(), resultEnd); + String unescapedRowset = unescapeXml(escapedRowset).trim(); + if (unescapedRowset.startsWith("")) { + debug("OFHStatement embedded ROWSET preview: " + preview(unescapedRowset)); + return unescapedRowset; + } + + return null; + } + + private ResultSet buildCsvResultSet(String responseContent) throws SQLException { + try { + Iterable parsedRecords = CSVFormat.DEFAULT.parse(new StringReader(responseContent)); + List records = new ArrayList<>(); + for (CSVRecord record : parsedRecords) { + records.add(record); + } + + if (records.isEmpty()) { + debug("OFHStatement CSV parser found no records"); + return emptyResultSet(); + } + + CSVRecord header = records.get(0); + debug("OFHStatement parser branch: CSV. Header columns: " + header.size() + " -> " + header); + if (records.size() > 1) { + CSVRecord firstRow = records.get(1); + debug("OFHStatement CSV first row columns: " + firstRow.size() + " preview: " + preview(firstRow.toString())); + } else { + debug("OFHStatement CSV has header only"); + } + + if (maxRows > 0 && records.size() > maxRows + 1) { + debug("OFHStatement applying maxRows to CSV result: " + maxRows); + records = new ArrayList<>(records.subList(0, maxRows + 1)); + } + + return new OFHResultSet(records); } catch (IOException e) { throw new SQLException("CSV Parsing Error: " + e.getMessage()); } - return new OFHResultSet(records); + } + private Document parseXmlDocument(String xmlContent) throws ParserConfigurationException, IOException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new InputSource(new StringReader(xmlContent))); } - private String getResponseCSVString(Document doc) { - String base64Content = getContentDataFromDoc(doc); + private ResultSet buildXmlResultSet(Document xmlResult) { + NodeList rowNodes = xmlResult.getDocumentElement().getChildNodes(); + List headers = new ArrayList<>(); + LinkedHashSet headerNames = new LinkedHashSet<>(); + List> rows = new ArrayList<>(); - byte[] decodedBytes = Base64.getDecoder().decode(base64Content); + for (Node rowNode : iterable(rowNodes)) { + if (!matchesNodeName(rowNode, "ROW")) { + continue; + } - String responseCsv = new String(decodedBytes); + Map rowValues = new LinkedHashMap<>(); + for (Node columnNode : iterable(rowNode.getChildNodes())) { + if (columnNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } - return responseCsv; + String columnName = columnNode.getNodeName(); + if (headerNames.add(columnName)) { + headers.add(columnName); + } + rowValues.put(columnName, columnNode.getTextContent()); + } + + List row = new ArrayList<>(); + for (String header : headers) { + row.add(rowValues.get(header)); + } + rows.add(row); + } + + debug("OFHStatement XML ROWSET parsed rows: " + rows.size() + " columns: " + headers.size() + " headers: " + headers); + + if (maxRows > 0 && rows.size() > maxRows) { + debug("OFHStatement applying maxRows to XML result: " + maxRows); + rows = new ArrayList<>(rows.subList(0, maxRows)); + } + + return new OFHResultSet(headers, rows); } - private String getContentDataFromDoc(Document doc) { + private String extractEmbeddedRowset(Node node) { + if (node == null) { + return null; + } - NodeList envelopeNodeList = doc.getElementsByTagName("env:Envelope"); + String textContent = stripBom(node.getTextContent()); + if (textContent != null) { + String unescapedText = unescapeXml(textContent).trim(); + if (unescapedText.startsWith("")) { + debug("OFHStatement embedded ROWSET preview: " + preview(unescapedText)); + return unescapedText; + } + } - if (envelopeNodeList.getLength() == 0) { - throw new RuntimeException("No Envelope found"); + for (Node childNode : iterable(node.getChildNodes())) { + String embeddedRowset = extractEmbeddedRowset(childNode); + if (embeddedRowset != null) { + return embeddedRowset; + } } - Node envelope = envelopeNodeList.item(0); + return null; + } - NodeList envelopeChildNodeList = envelope.getChildNodes(); + private String unescapeXml(String value) { + return value + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace("&", "&"); + } - if (envelopeChildNodeList.getLength() == 0) { - throw new RuntimeException("No Envelope child nodes found"); + private String stripBom(String value) { + if (value == null) { + return null; + } + if (!value.isEmpty() && value.charAt(0) == '\ufeff') { + return value.substring(1); } + return value; + } - Node body = null; + private boolean isEmptyDataSet(Node node) { + if (node == null || !matchesNodeName(node, "DATA_DS")) { + return false; + } - for (Node node : iterable(envelopeChildNodeList)) { - if (node.getNodeName().equals("env:Body")) { - body = node; - break; + return !hasNestedElementWithText(node); + } + + private boolean hasNestedElementWithText(Node node) { + for (Node childNode : iterable(node.getChildNodes())) { + if (childNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + String textContent = stripBom(childNode.getTextContent()); + if (textContent != null && !textContent.trim().isEmpty()) { + return true; + } + + if (hasNestedElementWithText(childNode)) { + return true; } } - if (body == null) { - throw new RuntimeException("No Body found"); + return false; + } + + private ResultSet emptyResultSet() { + return new OFHResultSet(new ArrayList<>(), new ArrayList<>()); + } + + private String preview(String value) { + if (value == null) { + return "null"; } - if (body.getChildNodes().getLength() == 0) { - throw new RuntimeException("No Body child nodes found"); + String normalized = value + .replace("\ufeff", "\\uFEFF") + .replace("\r", "\\r") + .replace("\n", "\\n"); + + int maxLength = 240; + if (normalized.length() <= maxLength) { + return normalized; } - Node runReportResponse = null; + return normalized.substring(0, maxLength) + "..."; + } - for (Node node : iterable(body.getChildNodes())) { - if (node.getNodeName().equals("runReportResponse")) { - runReportResponse = node; - break; - } + private void debug(String message) { + if (debugEnabled) { + System.out.println(message); } + } - if (runReportResponse == null) { - throw new RuntimeException("No runReportResponse found"); + private String getContentDataFromDoc(Document doc) { + + Node envelope = findFirstChild(doc, "Envelope"); + + if (envelope == null) { + throw new RuntimeException("No Envelope found"); } - if (runReportResponse.getChildNodes().getLength() == 0) { - throw new RuntimeException("No runReportResponse child nodes found"); + Node body = findFirstChild(envelope, "Body"); + + if (body == null) { + throw new RuntimeException("No Body found"); } - Node runReportReturn = null; + Node fault = findFirstChild(body, "Fault"); - for (Node node : iterable(runReportResponse.getChildNodes())) { - if (node.getNodeName().equals("runReportReturn")) { - runReportReturn = node; - break; - } + if (fault != null) { + String faultReason = extractSoapFaultReason(fault); + throw new RuntimeException("SOAP Fault: " + faultReason); + } + + Node runReportResponse = findFirstChild(body, "runReportResponse"); + + if (runReportResponse == null) { + throw new RuntimeException("No runReportResponse found"); } + Node runReportReturn = findFirstChild(runReportResponse, "runReportReturn"); + if (runReportReturn == null) { throw new RuntimeException("No runReportReturn found"); } - if (runReportReturn.getChildNodes().getLength() == 0) { - throw new RuntimeException("No runReportReturn child nodes found"); + Node reportBytes = findFirstChild(runReportReturn, "reportBytes"); + + if (reportBytes == null) { + throw new RuntimeException("No reportBytes found"); } - Node reportBytes = null; + return reportBytes.getTextContent(); + } - for (Node node : iterable(runReportReturn.getChildNodes())) { - if (node.getNodeName().equals("reportBytes")) { - reportBytes = node; - break; + private Node findFirstChild(Node parent, String nodeName) { + NodeList childNodes = parent.getChildNodes(); + for (Node node : iterable(childNodes)) { + if (matchesNodeName(node, nodeName)) { + return node; } } + return null; + } - if (reportBytes == null) { - throw new RuntimeException("No reportBytes found"); + private boolean matchesNodeName(Node node, String expectedName) { + if (node == null) { + return false; } - String base64Content = reportBytes.getTextContent(); + String localName = node.getLocalName(); + if (expectedName.equals(localName)) { + return true; + } - return base64Content; + String nodeName = node.getNodeName(); + return expectedName.equals(nodeName) || nodeName.endsWith(":" + expectedName); + } + + private String extractSoapFaultReason(Node fault) { + Node reason = findFirstChild(fault, "Reason"); + if (reason != null) { + Node text = findFirstChild(reason, "Text"); + if (text != null && text.getTextContent() != null && !text.getTextContent().isEmpty()) { + return text.getTextContent(); + } + } + + Node faultString = findFirstChild(fault, "faultstring"); + if (faultString != null && faultString.getTextContent() != null && !faultString.getTextContent().isEmpty()) { + return faultString.getTextContent(); + } + + String fallback = fault.getTextContent(); + if (fallback == null || fallback.isEmpty()) { + return "Unknown SOAP fault"; + } + return fallback.trim(); } public static Iterable iterable(final NodeList nodeList) { @@ -259,14 +516,42 @@ private void getOutput(HttpURLConnection conn, StringBuffer response, int respon } else { is = conn.getInputStream(); } - BufferedReader in = new BufferedReader(new InputStreamReader(is)); - String inputLine; - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); + if (is == null) { + return; + } + + byte[] responseBytes = readAllBytes(is); + byte[] decodedBytes = maybeDecompressResponse(conn, responseBytes); + response.append(new String(decodedBytes, StandardCharsets.UTF_8)); + is.close(); + } + + private byte[] maybeDecompressResponse(HttpURLConnection conn, byte[] responseBytes) throws IOException { + String contentEncoding = conn.getContentEncoding(); + boolean gzipEncoded = contentEncoding != null && contentEncoding.toLowerCase().contains("gzip"); + boolean gzipMagic = responseBytes.length >= 2 + && (responseBytes[0] & 0xff) == 0x1f + && (responseBytes[1] & 0xff) == 0x8b; + + if (!gzipEncoded && !gzipMagic) { + return responseBytes; + } + + debug("OFHStatement decompressing gzip response body"); + try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(responseBytes))) { + return readAllBytes(gzipInputStream); } + } - in.close(); + private byte[] readAllBytes(InputStream inputStream) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + return outputStream.toByteArray(); } private static String encodeXML(CharSequence s) { @@ -343,12 +628,15 @@ public void setMaxFieldSize(int i) throws SQLException { @Override public int getMaxRows() throws SQLException { - return 0; + return maxRows; } @Override public void setMaxRows(int i) throws SQLException { - + if (i < 0) { + throw new SQLException("maxRows cannot be negative"); + } + this.maxRows = i; } @Override diff --git a/src/test/java/com/oraclefusionhub/jdbc/OFHDriverTest.java b/src/test/java/com/oraclefusionhub/jdbc/OFHDriverTest.java index 76919b0..12afef1 100644 --- a/src/test/java/com/oraclefusionhub/jdbc/OFHDriverTest.java +++ b/src/test/java/com/oraclefusionhub/jdbc/OFHDriverTest.java @@ -1,6 +1,8 @@ package com.oraclefusionhub.jdbc; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import java.sql.Connection; import java.sql.Driver; @@ -64,4 +66,99 @@ public void test() throws SQLException { assertTrue("connection should be closed", connection.isClosed()); } -} \ No newline at end of file + @Test + public void testSoapFaultMessage() { + + mockXMLPServer.stubFor(WireMock.post(WireMock.urlEqualTo("/xmlpserver/services/ExternalReportWSSService")) + .willReturn(WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/xml") + .withBodyFile("E2ETestFault.xml"))); + + try (Connection connection = DriverManager.getConnection("jdbc:ofh://http://localhost:8089", "testUser", + "testPassword"); + Statement statement = connection.createStatement()) { + statement.executeQuery("select sysdate from dual"); + fail("Expected SQLException"); + } catch (SQLException e) { + assertEquals("Parsing Error: SOAP Fault: Invalid column type XMLTYPE for report output", e.getMessage()); + } + } + + @Test + public void testXmlRowsetResult() throws SQLException { + + mockXMLPServer.stubFor(WireMock.post(WireMock.urlEqualTo("/xmlpserver/services/ExternalReportWSSService")) + .willReturn(WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/xml") + .withBodyFile("E2ETestXmlResult.xml"))); + + try (Connection connection = DriverManager.getConnection("jdbc:ofh://http://localhost:8089", "testUser", + "testPassword"); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select sysdate from dual")) { + assertTrue("resultset should contain xml-derived row", resultSet.next()); + assertEquals("SYSDATE", resultSet.getMetaData().getColumnLabel(1)); + assertEquals("2026-04-12", resultSet.getString(1)); + assertEquals("2026-04-12", resultSet.getString("SYSDATE")); + } + } + + @Test + public void testWrappedXmlRowsetResult() throws SQLException { + + mockXMLPServer.stubFor(WireMock.post(WireMock.urlEqualTo("/xmlpserver/services/ExternalReportWSSService")) + .willReturn(WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/xml") + .withBodyFile("E2ETestWrappedXmlResult.xml"))); + + try (Connection connection = DriverManager.getConnection("jdbc:ofh://http://localhost:8089", "testUser", + "testPassword"); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select sysdate from dual")) { + assertTrue("resultset should contain wrapped xml-derived row", resultSet.next()); + assertEquals("SYSDATE", resultSet.getMetaData().getColumnLabel(1)); + assertEquals("2026-04-12", resultSet.getString(1)); + } + } + + @Test + public void testWrappedXmlRowsetRespectsMaxRows() throws SQLException { + + mockXMLPServer.stubFor(WireMock.post(WireMock.urlEqualTo("/xmlpserver/services/ExternalReportWSSService")) + .willReturn(WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/xml") + .withBodyFile("E2ETestWrappedXmlResult.xml"))); + + try (Connection connection = DriverManager.getConnection("jdbc:ofh://http://localhost:8089", "testUser", + "testPassword")) { + Statement fullStatement = connection.createStatement(); + fullStatement.setMaxRows(0); + ResultSet fullResultSet = fullStatement.executeQuery("select sysdate from dual"); + assertTrue(fullResultSet.next()); + assertEquals(false, fullResultSet.next()); + fullResultSet.close(); + fullStatement.close(); + + Statement limitedStatement = connection.createStatement(); + limitedStatement.setMaxRows(1); + ResultSet limitedResultSet = limitedStatement.executeQuery("select sysdate from dual"); + assertTrue(limitedResultSet.next()); + assertEquals(false, limitedResultSet.next()); + limitedResultSet.close(); + limitedStatement.close(); + } + } + + @Test + public void testEmptyWrappedXmlResult() throws SQLException { + + mockXMLPServer.stubFor(WireMock.post(WireMock.urlEqualTo("/xmlpserver/services/ExternalReportWSSService")) + .willReturn(WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/xml") + .withBodyFile("E2ETestEmptyWrappedResult.xml"))); + + try (Connection connection = DriverManager.getConnection("jdbc:ofh://http://localhost:8089", "testUser", + "testPassword"); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select sysdate from dual")) { + assertEquals(0, resultSet.getMetaData().getColumnCount()); + assertEquals(false, resultSet.next()); + } + } + +} diff --git a/src/test/java/com/oraclefusionhub/jdbc/OFHResultSetTest.java b/src/test/java/com/oraclefusionhub/jdbc/OFHResultSetTest.java index e77b042..92951d2 100644 --- a/src/test/java/com/oraclefusionhub/jdbc/OFHResultSetTest.java +++ b/src/test/java/com/oraclefusionhub/jdbc/OFHResultSetTest.java @@ -5,12 +5,17 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.Collections; public class OFHResultSetTest { @@ -36,4 +41,66 @@ public void testResultSet() throws IOException, SQLException { rs.close(); } -} \ No newline at end of file + @Test + public void testResultSetFromRows() throws SQLException { + ResultSet rs = new OFHResultSet( + Arrays.asList("SYSDATE"), + Collections.singletonList(Collections.singletonList("2026-04-12")) + ); + + assertEquals("SYSDATE", rs.getMetaData().getColumnLabel(1)); + assertEquals(1, rs.findColumn("SYSDATE")); + + rs.next(); + assertEquals("2026-04-12", rs.getString(1)); + assertEquals("2026-04-12", rs.getString("SYSDATE")); + assertEquals("2026-04-12", rs.getObject(1)); + } + + @Test + public void testEmptyResultSet() throws SQLException { + ResultSet rs = new OFHResultSet(Collections.emptyList(), Collections.emptyList()); + + assertEquals(0, rs.getMetaData().getColumnCount()); + assertFalse(rs.next()); + assertFalse(rs.first()); + assertFalse(rs.last()); + assertFalse(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + } + + @Test + public void testCursorNavigationAndWasNull() throws SQLException { + ResultSet rs = new OFHResultSet( + Arrays.asList("A", "B"), + Collections.singletonList(Collections.singletonList("a1")) + ); + + assertTrue(rs.next()); + assertEquals("a1", rs.getString(1)); + assertFalse(rs.wasNull()); + assertEquals(null, rs.getString(2)); + assertTrue(rs.wasNull()); + assertTrue(rs.isFirst()); + assertTrue(rs.isLast()); + assertEquals(1, rs.getRow()); + assertTrue(rs.first()); + rs.afterLast(); + assertTrue(rs.isAfterLast()); + rs.beforeFirst(); + assertTrue(rs.isBeforeFirst()); + } + + @Test + public void testMetadataTypeDefaults() throws SQLException { + ResultSet rs = new OFHResultSet( + Collections.singletonList("A"), + Collections.singletonList(Collections.singletonList("a1")) + ); + + assertEquals(Types.VARCHAR, rs.getMetaData().getColumnType(1)); + assertEquals("VARCHAR", rs.getMetaData().getColumnTypeName(1)); + assertEquals(String.class.getName(), rs.getMetaData().getColumnClassName(1)); + } + +} diff --git a/src/test/resources/__files/E2ETestEmptyWrappedResult.xml b/src/test/resources/__files/E2ETestEmptyWrappedResult.xml new file mode 100644 index 0000000..76a6fd4 --- /dev/null +++ b/src/test/resources/__files/E2ETestEmptyWrappedResult.xml @@ -0,0 +1,11 @@ + + + + + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLUdlbmVyYXRlZCBieSBPcmFjbGUgQW5hbHl0aWNzIFB1Ymxpc2hlciAtRGF0YWVuZ2luZS0tPgo8REFUQV9EUz48R18xPgo8L0dfMT48L0RBVEFfRFM+ + text/xml;charset=UTF-8 + + + + diff --git a/src/test/resources/__files/E2ETestFault.xml b/src/test/resources/__files/E2ETestFault.xml new file mode 100644 index 0000000..e806fa9 --- /dev/null +++ b/src/test/resources/__files/E2ETestFault.xml @@ -0,0 +1,13 @@ + + + + + + env:Receiver + + + Invalid column type XMLTYPE for report output + + + + diff --git a/src/test/resources/__files/E2ETestWrappedXmlResult.xml b/src/test/resources/__files/E2ETestWrappedXmlResult.xml new file mode 100644 index 0000000..bc14d08 --- /dev/null +++ b/src/test/resources/__files/E2ETestWrappedXmlResult.xml @@ -0,0 +1,11 @@ + + + + + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPERBVEFfRFM+PEdfMT4KPFJFU1VMVD4mbHQ7Uk9XU0VUIHhtbG5zOnhzaSA9ICZxdW90O2h0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlJnF1b3Q7Jmd0OwogJmx0O1JPVyZndDsKICAmbHQ7U1lTREFURSZndDsyMDI2LTA0LTEyJmx0Oy9TWVNEQVRFJmd0OwogJmx0Oy9ST1cmZ3Q7CiZsdDsvUk9XU0VUJmd0OzwvUkVTVUxUPjwvR18xPjwvREFUQV9EUz4= + text/xml;charset=UTF-8 + + + + diff --git a/src/test/resources/__files/E2ETestXmlResult.xml b/src/test/resources/__files/E2ETestXmlResult.xml new file mode 100644 index 0000000..ad87061 --- /dev/null +++ b/src/test/resources/__files/E2ETestXmlResult.xml @@ -0,0 +1,11 @@ + + + + + + PFJPV1NFVCB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIj48Uk9XPjxTWVNEQVRFPjIwMjYtMDQtMTI8L1NZU0RBVEU+PC9ST1c+PC9ST1dTRVQ+ + text/plain;charset=UTF-8 + + + + From b806b4b73eda41b31c73423768573a5af37fd50c Mon Sep 17 00:00:00 2001 From: satyapadala Date: Sun, 12 Apr 2026 21:39:13 +0530 Subject: [PATCH 2/2] Update upload-artifact action to v4 --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c80b47e..37c4627 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -31,7 +31,7 @@ jobs: run: mvn -B package --file pom.xml - name: Archive artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: OFHSqlDriver path: |