|
1 | 1 | /* |
2 | | - * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. |
| 2 | + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. |
3 | 3 | */ |
4 | 4 | package com.marklogic.client.query; |
5 | 5 |
|
6 | 6 | import com.marklogic.client.MarkLogicIOException; |
7 | 7 | import com.marklogic.client.impl.AbstractQueryDefinition; |
| 8 | +import com.marklogic.client.impl.DOMWriter; |
8 | 9 | import com.marklogic.client.impl.RawQueryDefinitionImpl; |
9 | 10 | import com.marklogic.client.impl.XmlFactories; |
10 | 11 | import com.marklogic.client.io.BaseHandle; |
|
20 | 21 |
|
21 | 22 | import javax.xml.XMLConstants; |
22 | 23 | import javax.xml.namespace.QName; |
| 24 | +import javax.xml.parsers.DocumentBuilder; |
23 | 25 | import javax.xml.stream.XMLOutputFactory; |
24 | 26 | import javax.xml.stream.XMLStreamException; |
25 | 27 | import javax.xml.stream.XMLStreamWriter; |
26 | 28 | import javax.xml.transform.Templates; |
| 29 | +import org.w3c.dom.Document; |
| 30 | +import org.xml.sax.SAXException; |
| 31 | +import java.io.ByteArrayInputStream; |
27 | 32 | import java.io.ByteArrayOutputStream; |
28 | 33 | import java.io.IOException; |
29 | 34 | import java.io.OutputStream; |
| 35 | +import java.nio.charset.StandardCharsets; |
30 | 36 | import java.util.Calendar; |
31 | 37 | import java.util.Date; |
32 | 38 | import java.util.HashMap; |
@@ -289,6 +295,35 @@ public RawStructuredQueryDefinition build(StructuredQueryDefinition... queries) |
289 | 295 | ); |
290 | 296 | } |
291 | 297 |
|
| 298 | + /** |
| 299 | + * Defines one or more annotation elements to include in the query. An annotation is a marker |
| 300 | + * element that is ignored when the query is evaluated; it can hold arbitrary XML (or plain text) |
| 301 | + * and is typically used to document a query or to mark parts of a query so they can be located and |
| 302 | + * manipulated later. When the structured query is turned into a |
| 303 | + * <a href="http://docs.marklogic.com/guide/search-dev/cts_query#id_28038">cts:query</a> by the |
| 304 | + * server, each annotation becomes a {@code cts:annotation} element. |
| 305 | + * <p> |
| 306 | + * Each argument becomes the content of a separate {@code annotation} element and may be a well-formed |
| 307 | + * XML fragment (including elements in their own namespaces) or plain text. Annotations are valid at |
| 308 | + * the top level of a query, so the resulting query definition should be passed to |
| 309 | + * {@link #build(StructuredQueryDefinition...)} alongside the other query definitions, for example: |
| 310 | + * <pre>{@code |
| 311 | + * qb.build( |
| 312 | + * qb.and(qb.term("hello"), qb.term("world")), |
| 313 | + * qb.annotation("<a:marker xmlns:a='http://example.org/annotations'>roles-and-rights</a:marker>")); |
| 314 | + * }</pre> |
| 315 | + * @param annotations the content of one or more annotation elements |
| 316 | + * @return the StructuredQueryDefinition for the annotations |
| 317 | + * @see <a href="http://docs.marklogic.com/guide/search-dev/cts_query#id_28038"> |
| 318 | + * Composing cts:query Expressions: cts:annotation</a> |
| 319 | + */ |
| 320 | + public StructuredQueryDefinition annotation(String... annotations) { |
| 321 | + if (annotations == null) { |
| 322 | + throw new IllegalArgumentException("annotations cannot be null"); |
| 323 | + } |
| 324 | + return new AnnotationQuery(annotations); |
| 325 | + } |
| 326 | + |
292 | 327 | /** |
293 | 328 | * Defines an AND query over the list of query definitions. |
294 | 329 | * @param queries the query definitions |
@@ -1272,6 +1307,65 @@ public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException |
1272 | 1307 | } |
1273 | 1308 | } |
1274 | 1309 |
|
| 1310 | + protected class AnnotationQuery |
| 1311 | + extends AbstractStructuredQuery { |
| 1312 | + private String[] annotations; |
| 1313 | + |
| 1314 | + public AnnotationQuery(String... annotations) { |
| 1315 | + super(); |
| 1316 | + this.annotations = annotations; |
| 1317 | + } |
| 1318 | + |
| 1319 | + @Override |
| 1320 | + public void innerSerialize(XMLStreamWriter serializer) throws XMLStreamException { |
| 1321 | + if (annotations == null) { |
| 1322 | + return; |
| 1323 | + } |
| 1324 | + for (String annotation: annotations) { |
| 1325 | + writeSearchElement(serializer, "annotation"); |
| 1326 | + if (annotation != null) { |
| 1327 | + writeAnnotationContent(serializer, annotation); |
| 1328 | + } |
| 1329 | + serializer.writeEndElement(); |
| 1330 | + } |
| 1331 | + } |
| 1332 | + |
| 1333 | + // The content of an annotation is arbitrary XML (or plain text). Attempt to parse it as an XML |
| 1334 | + // fragment and copy the resulting nodes into the serializer; if it is not well-formed XML, fall |
| 1335 | + // back to writing it as text so plain-text annotations are still supported. |
| 1336 | + private void writeAnnotationContent(XMLStreamWriter serializer, String annotation) |
| 1337 | + throws XMLStreamException |
| 1338 | + { |
| 1339 | + Document document = parseAnnotationContent(annotation); |
| 1340 | + if (document == null) { |
| 1341 | + serializer.writeCharacters(annotation); |
| 1342 | + return; |
| 1343 | + } |
| 1344 | + new DOMWriter(serializer).serializeNodeList(document.getDocumentElement().getChildNodes()); |
| 1345 | + } |
| 1346 | + |
| 1347 | + private Document parseAnnotationContent(String annotation) { |
| 1348 | + String wrapped = "<annotation-content>" + annotation + "</annotation-content>"; |
| 1349 | + try { |
| 1350 | + DocumentBuilder builder = XmlFactories.getDocumentBuilderFactory().newDocumentBuilder(); |
| 1351 | + return builder.parse( |
| 1352 | + new ByteArrayInputStream(wrapped.getBytes(StandardCharsets.UTF_8))); |
| 1353 | + } catch (SAXException e) { |
| 1354 | + // Not well-formed XML; treat the content as plain text. |
| 1355 | + return null; |
| 1356 | + } catch (IOException | RuntimeException | javax.xml.parsers.ParserConfigurationException e) { |
| 1357 | + throw new MarkLogicIOException(e); |
| 1358 | + } |
| 1359 | + } |
| 1360 | + |
| 1361 | + @Override |
| 1362 | + public boolean canSerializeQueryAsJSON() { |
| 1363 | + // Annotations carry arbitrary XML content that has no JSON structured-query representation, |
| 1364 | + // so force the XML serialization path. |
| 1365 | + return false; |
| 1366 | + } |
| 1367 | + } |
| 1368 | + |
1275 | 1369 | protected class OrQuery |
1276 | 1370 | extends AbstractStructuredQuery { |
1277 | 1371 | private StructuredQueryDefinition[] queries; |
@@ -2600,7 +2694,16 @@ private void writeStructuredQueryImpl(OutputStream out, Object... objects) { |
2600 | 2694 | } |
2601 | 2695 | } |
2602 | 2696 | for (AbstractStructuredQuery query: (AbstractStructuredQuery[]) objects) { |
2603 | | - query.innerSerialize(serializer); |
| 2697 | + if (!(query instanceof AnnotationQuery)) { |
| 2698 | + query.innerSerialize(serializer); |
| 2699 | + } |
| 2700 | + } |
| 2701 | + // The search schema requires annotation elements to follow the query elements within the |
| 2702 | + // top-level query element, so serialize any annotations last regardless of caller order. |
| 2703 | + for (AbstractStructuredQuery query: (AbstractStructuredQuery[]) objects) { |
| 2704 | + if (query instanceof AnnotationQuery) { |
| 2705 | + query.innerSerialize(serializer); |
| 2706 | + } |
2604 | 2707 | } |
2605 | 2708 | } else if (objects instanceof RegionImpl[]) { |
2606 | 2709 | for (RegionImpl region: (RegionImpl[]) objects) { |
|
0 commit comments