From 213f0abb564867f12fe17b768e3821762d8243ca Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 28 Mar 2023 14:53:19 -0400 Subject: [PATCH] DEVEXP-358 Can now serialize a query using STaX --- .../client/query/StructuredQueryBuilder.java | 104 ++++++++++-------- .../query/StructuredQueryDefinition.java | 26 ++++- .../test/StructuredQueryBuilderTest.java | 42 +++++++ 3 files changed, 122 insertions(+), 50 deletions(-) diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryBuilder.java b/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryBuilder.java index 4c8921445..c42400752 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryBuilder.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryBuilder.java @@ -1231,6 +1231,11 @@ public AbstractStructuredQuery withCriteria(String criteria) { return this; } + @Override + public void serialize(XMLStreamWriter serializer) throws XMLStreamException { + innerSerialize(serializer); + } + @Override public String serialize() { return serializeQueries(this); @@ -1302,7 +1307,7 @@ public NotInQuery(StructuredQueryDefinition positive, StructuredQueryDefinition @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("not-in-query"); + writeSearchElement(serializer, "not-in-query"); writeQuery(serializer, "positive-query", (AbstractStructuredQuery) positive); writeQuery(serializer, "negative-query", (AbstractStructuredQuery) negative); serializer.writeEndElement(); @@ -1322,7 +1327,7 @@ public AndNotQuery(StructuredQueryDefinition positive, StructuredQueryDefinition @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("and-not-query"); + writeSearchElement(serializer, "and-not-query"); writeQuery(serializer, "positive-query", (AbstractStructuredQuery) positive); writeQuery(serializer, "negative-query", (AbstractStructuredQuery) negative); serializer.writeEndElement(); @@ -1342,7 +1347,7 @@ public BoostQuery(StructuredQueryDefinition matchingQuery, StructuredQueryDefini @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("boost-query"); + writeSearchElement(serializer, "boost-query"); writeQuery(serializer, "matching-query", (AbstractStructuredQuery) matchingQuery); writeQuery(serializer, "boosting-query", (AbstractStructuredQuery) boostingQuery); serializer.writeEndElement(); @@ -1361,7 +1366,7 @@ public DocumentQuery(String... uris) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("document-query"); + writeSearchElement(serializer, "document-query"); writeTextList(serializer, "uri", uris); serializer.writeEndElement(); } @@ -1380,7 +1385,7 @@ public TermQuery(Double weight, String... terms) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("term-query"); + writeSearchElement(serializer, "term-query"); writeTextList(serializer, "text", terms); writeText(serializer, "weight", weight); serializer.writeEndElement(); @@ -1411,10 +1416,10 @@ public NearQuery(Integer minimumDistance, Integer maximumDistance, Double weight @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("near-query"); + writeSearchElement(serializer, "near-query"); writeQueryList(serializer, queries); if (order != null) { - serializer.writeStartElement("ordered"); + writeSearchElement(serializer, "ordered"); serializer.writeCharacters( Boolean.toString(order == Ordering.ORDERED)); serializer.writeEndElement(); @@ -1437,7 +1442,7 @@ public CollectionQuery(String... uris) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("collection-query"); + writeSearchElement(serializer, "collection-query"); writeTextList(serializer, "uri", uris); serializer.writeEndElement(); } @@ -1465,7 +1470,7 @@ public DirectoryQuery(Boolean isInfinite, String... uris) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("directory-query"); + writeSearchElement(serializer, "directory-query"); if (depth != null) { serializer.writeAttribute("depth", Integer.toString(depth)); } @@ -1544,7 +1549,7 @@ public ContainerConstraintQuery(String constraintName, StructuredQueryDefinition @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("container-constraint-query"); + writeSearchElement(serializer, "container-constraint-query"); writeText(serializer, "constraint-name", name); writeQuery(serializer, query); serializer.writeEndElement(); @@ -1564,7 +1569,7 @@ public PropertiesConstraintQuery(String constraintName, StructuredQueryDefinitio @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("properties-constraint-query"); + writeSearchElement(serializer, "properties-constraint-query"); writeText(serializer, "constraint-name", name); writeQuery(serializer, query); serializer.writeEndElement(); @@ -1584,7 +1589,7 @@ public CollectionConstraintQuery(String constraintName, String... uris) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("collection-constraint-query"); + writeSearchElement(serializer, "collection-constraint-query"); writeText(serializer, "constraint-name", name); writeTextList(serializer, "uri", uris); serializer.writeEndElement(); @@ -1611,7 +1616,7 @@ public ValueConstraintQuery(String constraintName, Double weight, String... valu @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("value-constraint-query"); + writeSearchElement(serializer, "value-constraint-query"); writeText(serializer, "constraint-name", name); writeTextList(serializer, "text", values); writeText(serializer, "weight", weight); @@ -1639,7 +1644,7 @@ public WordConstraintQuery(String constraintName, Double weight, String... words @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("word-constraint-query"); + writeSearchElement(serializer, "word-constraint-query"); writeText(serializer, "constraint-name", name); writeTextList(serializer, "text", words); writeText(serializer, "weight", weight); @@ -1662,7 +1667,7 @@ public RangeConstraintQuery(String constraintName, Operator operator, String... @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("range-constraint-query"); + writeSearchElement(serializer, "range-constraint-query"); writeText(serializer, "constraint-name", name); writeTextList(serializer, "value", values); writeText(serializer, "range-operator", operator); @@ -1683,7 +1688,7 @@ public GeospatialConstraintQuery(String constraintName, Region... regions) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("geospatial-constraint-query"); + writeSearchElement(serializer, "geospatial-constraint-query"); writeText(serializer, "constraint-name", name); for (Region region : regions) { ((RegionImpl) region).innerSerialize(serializer); @@ -1706,7 +1711,7 @@ public GeospatialRegionConstraintQuery(String constraintName, GeospatialOperator @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("geo-region-constraint-query"); + writeSearchElement(serializer, "geo-region-constraint-query"); writeText(serializer, "constraint-name", name); writeText(serializer, "geospatial-operator", operator.toString()); for (Region region : regions) { @@ -1729,7 +1734,7 @@ public CustomConstraintQuery(String constraintName, String... terms) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("custom-constraint-query"); + writeSearchElement(serializer, "custom-constraint-query"); writeText(serializer, "constraint-name", name); writeTextList(serializer, "text", terms); serializer.writeEndElement(); @@ -1746,7 +1751,7 @@ protected class ContainerQuery } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("container-query"); + writeSearchElement(serializer, "container-query"); ((IndexImpl) index).innerSerialize(serializer); writeQuery(serializer, query); serializer.writeEndElement(); @@ -1803,7 +1808,7 @@ protected class ValueQuery } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("value-query"); + writeSearchElement(serializer, "value-query"); if ( values != null && values.length > 0 ) { if ( values[0] == null ) { serializer.writeAttribute("type", "null"); @@ -1848,7 +1853,7 @@ protected class WordQuery } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("word-query"); + writeSearchElement(serializer, "word-query"); super.innerSerialize(serializer); serializer.writeEndElement(); } @@ -1909,7 +1914,7 @@ String formatValue(Object value, String type) { } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("range-query"); + writeSearchElement(serializer, "range-query"); if (type != null) { serializer.writeAttribute("type", type); if (collation!= null) { @@ -1974,7 +1979,7 @@ else if (index instanceof GeoPointPathImpl) { throw new IllegalStateException( "unknown index class: "+index.getClass().getName()); - serializer.writeStartElement(elemName); + writeSearchElement(serializer, elemName); ((IndexImpl) index).innerSerialize(serializer); if (scope != null) { writeText(serializer, "fragment-scope", @@ -2003,7 +2008,7 @@ protected class GeospatialRegionQuery extends GeospatialBaseQuery { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { String elemName = "geo-region-path-query"; - serializer.writeStartElement(elemName); + writeSearchElement(serializer, elemName); if(index.coordinateSystem != null) { serializer.writeAttribute("coord", index.coordinateSystem.toString()); } @@ -2043,7 +2048,7 @@ protected class TemporalPeriod } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("period"); + writeSearchElement(serializer, "period"); writeText(serializer, "period-start", formattedStart); writeText(serializer, "period-end", formattedEnd); serializer.writeEndElement(); @@ -2064,7 +2069,7 @@ protected class TemporalPeriodRangeQuery } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("period-range-query"); + writeSearchElement(serializer, "period-range-query"); writeTextList(serializer, "axis", axes); writeText(serializer, "temporal-operator", operator.toString().toLowerCase()); for ( Period period : periods ) { @@ -2089,7 +2094,7 @@ protected class TemporalPeriodCompareQuery } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("period-compare-query"); + writeSearchElement(serializer, "period-compare-query"); writeText(serializer, "axis1", axis1); writeText(serializer, "temporal-operator", operator.toString().toLowerCase()); writeText(serializer, "axis2", axis2); @@ -2114,7 +2119,7 @@ protected class TemporalLsqtQuery } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("lsqt-query"); + writeSearchElement(serializer, "lsqt-query"); writeText(serializer, "temporal-collection", temporalCollection); if ( formattedTimestamp != null && formattedTimestamp.length() > 0 ) { writeText(serializer, "timestamp", formattedTimestamp); @@ -2124,7 +2129,7 @@ public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException serializer.writeEndElement(); } } - + protected class TimeQuery extends AbstractStructuredQuery { private String formattedTimestamp = null; private String startElement = null; @@ -2137,7 +2142,7 @@ protected class TimeQuery extends AbstractStructuredQuery { } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement(startElement); + writeSearchElement(serializer, startElement); if (formattedTimestamp != null && formattedTimestamp.length() > 0) { writeText(serializer, "timestamp", formattedTimestamp); } @@ -2198,7 +2203,7 @@ protected class FieldImpl extends IndexImpl implements Field { } @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("field"); + writeSearchElement(serializer, "field"); serializer.writeAttribute("name", name); serializer.writeEndElement(); } @@ -2398,7 +2403,7 @@ public double getLongitude() { } public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("point"); + writeSearchElement(serializer, "point"); writeText(serializer, "latitude", String.valueOf(lat)); writeText(serializer, "longitude", String.valueOf(lon)); serializer.writeEndElement(); @@ -2418,7 +2423,7 @@ public CircleImpl(double latitude, double longitude, double radius) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("circle"); + writeSearchElement(serializer, "circle"); writeText(serializer, "radius", String.valueOf(radius)); center.innerSerialize(serializer); serializer.writeEndElement(); @@ -2439,7 +2444,7 @@ public BoxImpl(double south, double west, double north, double east) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("box"); + writeSearchElement(serializer, "box"); writeText(serializer, "south", String.valueOf(south)); writeText(serializer, "west", String.valueOf(west)); writeText(serializer, "north", String.valueOf(north)); @@ -2459,7 +2464,7 @@ public PolygonImpl(Point... points) { @Override public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { - serializer.writeStartElement("polygon"); + writeSearchElement(serializer, "polygon"); for (Point point: points) { point.innerSerialize(serializer); } @@ -2540,7 +2545,7 @@ private void writeStructuredQueryImpl(OutputStream out, Object... objects) { // omit the XML prolog // serializer.writeStartDocument(); - serializer.writeStartElement(SEARCH_API_NS, "query"); + writeSearchElement(serializer, "query"); if (objects != null) { if (objects instanceof AbstractStructuredQuery[]) { @@ -2572,7 +2577,7 @@ static private void serializeNamedIndex(XMLStreamWriter serializer, String elemName, QName qname, String name) throws XMLStreamException { - serializer.writeStartElement(elemName); + writeSearchElement(serializer, elemName); if (qname != null) { String ns = qname.getNamespaceURI(); serializer.writeAttribute("ns", (ns != null) ? ns : ""); @@ -2591,7 +2596,7 @@ static private void writeText(XMLStreamWriter serializer, if (object == null) { return; } - serializer.writeStartElement(container); + writeSearchElement(serializer, container); serializer.writeCharacters( (object instanceof String) ? (String) object : object.toString() @@ -2607,7 +2612,7 @@ static private void writeTextList(XMLStreamWriter serializer, } for (Object object: objects) { if ( object == null ) continue; - serializer.writeStartElement(container); + writeSearchElement(serializer, container); serializer.writeCharacters( (object instanceof String) ? (String) object : object.toString() @@ -2636,7 +2641,7 @@ static private void writeQuery(XMLStreamWriter serializer, String container, AbstractStructuredQuery query) throws XMLStreamException { - serializer.writeStartElement(container); + writeSearchElement(serializer, container); if (query != null) { query.innerSerialize(serializer); } @@ -2646,7 +2651,7 @@ static private void writeQueryList(XMLStreamWriter serializer, String container, AbstractStructuredQuery... queries) throws XMLStreamException { - serializer.writeStartElement(container); + writeSearchElement(serializer, container); if (queries != null) { for (AbstractStructuredQuery query: queries) { query.innerSerialize(serializer); @@ -2966,14 +2971,14 @@ public StructuredQueryDefinition temporalLsqtQuery(String temporalCollection, St if ( temporalCollection == null ) throw new IllegalArgumentException("temporalCollection cannot be null"); return new TemporalLsqtQuery(temporalCollection, timestamp, weight, options); } - + /** * Matches documents with timestamp prior to the given timestamp. * @param timestamp time in ISO 8601 format - documents with timestamp equal to or * prior to this timestamp will match * @return a query to filter. */ - public StructuredQueryDefinition beforeQuery(long timestamp) + public StructuredQueryDefinition beforeQuery(long timestamp) { if (timestamp == 0) throw new IllegalArgumentException("timestamp cannot be zero"); return new TimeQuery(Long.toUnsignedString(timestamp), "before-query"); @@ -2984,9 +2989,20 @@ public StructuredQueryDefinition beforeQuery(long timestamp) * @param timestamp time in ISO 8601 format - documents with timestamp after this time will match. * @return a query to filter. */ - public StructuredQueryDefinition afterQuery(long timestamp) + public StructuredQueryDefinition afterQuery(long timestamp) { if (timestamp == 0) throw new IllegalArgumentException("timestamp cannot be zero"); return new TimeQuery(Long.toUnsignedString(timestamp), "after-query"); } + + /** + * Convenience method for writing an element in the "search" namespace. + * + * @param serializer + * @param elementName + * @throws XMLStreamException + */ + private static void writeSearchElement(XMLStreamWriter serializer, String elementName) throws XMLStreamException { + serializer.writeStartElement(SEARCH_API_NS, elementName); + } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryDefinition.java b/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryDefinition.java index 1569d1a74..fb402ab4f 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryDefinition.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/query/StructuredQueryDefinition.java @@ -17,6 +17,9 @@ import com.marklogic.client.pojo.PojoQueryDefinition; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + /** * A StructuredQueryDefinition represents a structured query. * @@ -25,12 +28,23 @@ public interface StructuredQueryDefinition extends QueryDefinition, ValueQueryDefinition, PojoQueryDefinition { - /** - * Returns the structured query definition as a serialized XML string. - * - * @return The serialized definition. - */ - String serialize(); + /** + * Returns the structured query definition as a serialized XML string. + * + * @return The serialized definition. + */ + String serialize(); + + /** + * Serializes the structured query definition to the given XML stream writer. To ensure that the query elements are + * written to the correct namespace, you must set the {@code XMLOutputFactory.IS_REPAIRING_NAMESPACES} property on + * the {@code XMLOutputFactory} to "true". You may also wish to specify a prefix for the MarkLogic search namespace; + * you may do so via e.g. {@code xmlStreamWriter.setPrefix("search", "http://marklogic.com/appservices/search");}. + * + * @param xmlStreamWriter The XML stream writer to which the query definition should be serialized. + * @throws XMLStreamException + */ + void serialize(XMLStreamWriter xmlStreamWriter) throws XMLStreamException; /** * Returns the query criteria, that is the query string. diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/StructuredQueryBuilderTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/StructuredQueryBuilderTest.java index a23e22f52..c168e32d5 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/StructuredQueryBuilderTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/StructuredQueryBuilderTest.java @@ -20,6 +20,7 @@ import com.marklogic.client.query.StructuredQueryBuilder.Operator; import com.marklogic.client.query.StructuredQueryDefinition; import com.marklogic.client.util.EditableNamespaceContext; +import com.marklogic.rest.util.Fragment; import org.custommonkey.xmlunit.SimpleNamespaceContext; import org.custommonkey.xmlunit.XMLUnit; import org.custommonkey.xmlunit.XpathEngine; @@ -32,6 +33,8 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; @@ -73,6 +76,45 @@ static class StringInputStream extends ByteArrayInputStream { } } + @Test + void serializeQueryIntoStaxDocument() throws Exception { + StructuredQueryBuilder qb = Common.newClient().newQueryManager().newStructuredQueryBuilder(); + StructuredQueryDefinition query = qb.and(qb.collection("hello"), qb.term("world")); + + XMLOutputFactory factory = XMLOutputFactory.newFactory(); + factory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + + StringWriter writer = new StringWriter(); + XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(writer); + xmlWriter.setPrefix("example", "http://example.org/"); + xmlWriter.setPrefix("search", "http://marklogic.com/appservices/search"); + + xmlWriter.writeStartElement("http://example.org/", "myDocument"); + xmlWriter.writeStartElement("someLocalElement"); + xmlWriter.writeEndElement(); + query.serialize(xmlWriter); + xmlWriter.writeEndElement(); + xmlWriter.flush(); + xmlWriter.close(); + + String xml = new Fragment(writer.toString()).getPrettyXml(); + + final String expectedXml = "\n" + + " \n" + + " \n" + + " \n" + + " hello\n" + + " \n" + + " \n" + + " world\n" + + " \n" + + " \n" + + ""; + + assertXMLEqual("The query should have been embedded correctly in the XML document, with the Search namespace " + + "being associated with the 'search' prefix.", expectedXml, xml); + } + @SuppressWarnings("deprecation") @Test public void testBuilder() throws IOException, SAXException, ParserConfigurationException {