Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ clojure = "1.12.2"
jaxb-runtime = "2.3.2"
kaptcha = "0.0.8"
spring-websocket = "6.0.11"
jaxen = "2.0.0"

# IOC JCACHE

Expand Down Expand Up @@ -158,7 +159,7 @@ clojure = { module = "org.clojure:clojure", version.ref = "clojure" }
jaxb-runtime = { module = "org.glassfish.jaxb:jaxb-runtime", version.ref = "jaxb-runtime" }
kaptcha = { module = "com.github.axet:kaptcha", version.ref = "kaptcha" }
spring-websocket = { module = "org.springframework:spring-websocket", version.ref = "spring-websocket" }

jaxen = { module = "jaxen:jaxen", version.ref = "jaxen" }
# IOC JCACHE

javax-cache-api = { module = "javax.cache:cache-api", version.ref = "javax-cache" }
Expand Down
2 changes: 2 additions & 0 deletions tapestry-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ dependencies {
implementation libs.commons.codec
implementation libs.commons.lang3

implementation libs.jaxen

provided project(':tapestry-test')
provided project(':tapestry-test-constants')

Expand Down
182 changes: 182 additions & 0 deletions tapestry-core/src/main/java/org/apache/tapestry5/dom/BoundXPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.dom;

import java.util.List;

import org.apache.tapestry5.dom.xpath.XPath;
import org.apache.tapestry5.dom.xpath.XPathException;

/**
* An {@link XPath} pre-bound to a context {@link Node}.
* <p>
* This allows fluent, node-first queries without repeating the context node:
* <pre>
* List&lt;Element&gt; items = element.xpath("ul/li").elements();
* String text = document.xpath("id('header')").singleNormalizedDescendantText();
* </pre>
*
* @since 5.10
* @see Node#xpath(String)
* @see XPath
*/
public class BoundXPath
{
private final XPath xpath;

private final Object contextNode;

BoundXPath(XPath xpath, Object contextNode)
{
assert xpath != null;
assert contextNode != null;

this.xpath = xpath;
this.contextNode = contextNode;
}

/**
* Evaluates the XPath and returns all matching DOM nodes.
*
* @return list of matching {@link Node}s; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#selectNodes(Object)
*/
public List<Node> nodes()
{
return xpath.selectNodes(contextNode);
}

/**
* As {@link #nodes()} but assumes every result is an {@link Element}.
*
* @return list of matched {@link Element}s; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#selectElements(Object)
*/
public List<Element> elements()
{
return xpath.selectElements(contextNode);
}

/**
* As {@link #nodes()} but assumes every result is an {@link Attribute}.
*
* @return list of matched {@link Attribute}s; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#selectAttributes(Object)
*/
public List<Attribute> attributes()
{
return xpath.selectAttributes(contextNode);
}

/**
* Returns the first matching node, or {@code null} if there is no match.
*
* @return the first matched node, or {@code null}
* @throws XPathException if evaluation fails
* @see XPath#selectSingleNode(Object)
*/
public Object singleNode()
{
return xpath.selectSingleNode(contextNode);
}

/**
* As {@link #singleNode()} but assumes the result is an {@link Element}.
*
* @return the first matched {@link Element}, or {@code null}
* @throws XPathException if evaluation fails
* @see XPath#selectSingleElement(Object)
*/
public Element singleElement()
{
return xpath.selectSingleElement(contextNode);
}

/**
* Returns the XPath string-value of each matched node.
*
* @return list of string values; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#stringValuesOf(Object)
*/
public List<String> stringValuesOf()
{
return xpath.stringValuesOf(contextNode);
}

/**
* Returns {@link Element#getChildMarkup()} for each matched element.
*
* @return list of child-markup strings; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#selectElementsChildMarkup(Object)
*/
public List<String> elementsChildMarkup()
{
return xpath.selectElementsChildMarkup(contextNode);
}

/**
* Returns {@link Element#getAttribute(String)} for each matched element.
* <p>
* Unlike XPath attribute selection, elements that lack the attribute contribute
* a {@code null} entry so the result list is always the same size as the matched
* element list.
*
* @param attributeName name of the attribute to retrieve; must not be {@code null} or empty
* @return list of attribute values, with {@code null} for missing attributes; never {@code null}
* @throws IllegalArgumentException if {@code attributeName} is {@code null} or empty
* @throws XPathException if evaluation fails
* @see XPath#selectElementsAttribute(Object, String)
*/
public List<String> elementsAttribute(String attributeName)
{
if (attributeName == null || attributeName.isEmpty())
{
throw new IllegalArgumentException("attributeName must not be null or empty");
}

return xpath.selectElementsAttribute(contextNode, attributeName);
}

/**
* Selects a single node and concatenates all its descendant text, then normalises
* whitespace via {@link XPath#normalizeText(String)}.
*
* @return normalised text string; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#singleNormalizedDescendantText(Object)
*/
public String singleNormalizedDescendantText()
{
return xpath.singleNormalizedDescendantText(contextNode);
}

/**
* For each matched node, concatenates all descendant text and normalises
* whitespace via {@link XPath#normalizeText(String)}.
*
* @return list of normalised text strings, one per matched node; never {@code null}
* @throws XPathException if evaluation fails
* @see XPath#normalizedDescendantText(Object)
*/
public List<String> normalizedDescendantText()
{
return xpath.normalizedDescendantText(contextNode);
}
}
22 changes: 21 additions & 1 deletion tapestry-core/src/main/java/org/apache/tapestry5/dom/CData.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2007, 2008, 2009 The Apache Software Foundation
// Copyright 2007-2009, 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -32,6 +32,15 @@ public CData(Element container, String content)
this.content = content;
}

/**
* Returns the raw character content stored in this node, without any CDATA wrapping or entity encoding.
* @since 5.10
*/
public String getContent()
{
return content;
}

@Override
void toMarkup(Document document, PrintWriter writer, Map<String, String> namespaceURIToPrefix)
{
Expand All @@ -49,4 +58,15 @@ void toMarkup(Document document, PrintWriter writer, Map<String, String> namespa

writer.print(model.encode(content));
}

/**
* Returns a deep copy of this CDATA node, detached from any parent.
*
* @since 5.10
*/
@Override
public CData deepClone()
{
return new CData(null, content);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2006, 2008, 2009, 2010 The Apache Software Foundation
// Copyright 2006, 2008-2010, 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,4 +38,15 @@ void toMarkup(Document document, PrintWriter writer, Map<String, String> namespa
writer.print(comment);
writer.print("-->");
}

/**
* Returns a deep copy of this comment node, detached from any parent.
*
* @since 5.10
*/
@Override
public Comment deepClone()
{
return new Comment(null, comment);
}
}
49 changes: 47 additions & 2 deletions tapestry-core/src/main/java/org/apache/tapestry5/dom/Document.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2006, 2007, 2008, 2009, 2010, 2014 The Apache Software Foundation
// Copyright 2006-2010, 2014, 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -248,6 +248,18 @@ void visit(Visitor visitor)
rootElement.visit(visitor);
}

/**
* Visits all nodes in the document tree, starting from the root element, invoking the
* appropriate {@link NodeVisitor} method for each node type encountered.
*
* @param visitor callback
* @since 5.10
*/
public void visit(NodeVisitor visitor)
{
rootElement.visit(visitor);
}

private <T extends Node> T newChild(T child)
{
if (preamble == null)
Expand Down Expand Up @@ -305,6 +317,40 @@ public CData cdata(String content)
return newChild(new CData(null, content));
}

/**
* Returns an independent deep copy of this document, including its DTD, preamble nodes, and
* entire element tree.
*
* @return a fully independent deep copy of this document
* @since 5.10
*/
@Override
public Document deepClone()
{
Document clone = new Document(model, encoding, mimeType);

// DTD is immutable after construction, so sharing the reference is safe.
clone.dtd = dtd;

if (preamble != null)
{
clone.preamble = CollectionFactory.newList();
for (Node n : preamble)
clone.preamble.add(n.deepClone());
}

if (rootElement != null)
{
// Use the Document-accepting constructor so the clone's root element
// has its 'document' back-reference set correctly.
Element cloneRoot = new Element(clone, rootElement.getNamespace(), rootElement.getName());
clone.rootElement = cloneRoot;
rootElement.copyInto(cloneRoot);
}

return clone;
}

/**
* Returns the MIME type of this document.
* @return the MIME type.
Expand All @@ -314,5 +360,4 @@ public String getMimeType()
{
return mimeType;
}

}
Loading