diff --git a/README.md b/README.md index 5ba798b..657c3d1 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Full list of supported directives in the current version: * `ADDIF`: adds new node, if it's absent * `SET`: sets text value of current node * `XSET`: sets text value, calculating it with XPath + * `XATTR`: sets attribute value, calculating it with XPath * `CDATA`: same as `SET`, but makes `CDATA` * `UP`: moves cursor one node up * `XPATH`: moves cursor to the nodes found by XPath @@ -202,6 +203,19 @@ XSET "sum(/products/price) div count(/products)"; `XSET` doesn't move the cursor anywhere. +### XATTR + +`XATTR` changes the value of an attribute of all current nodes to a value +calculated with XPath expression: + +```assembly +ADD "product-1"; +ADD "price"; +XATTR "s", "sum(/products/price) div count(/products)"; +``` + +`XATTR` doesn't move the cursor anywhere. + ### UP `UP` moves all current nodes to their parents. diff --git a/src/main/antlr4/org/xembly/Xembly.g4 b/src/main/antlr4/org/xembly/Xembly.g4 index 504b185..6d7e916 100644 --- a/src/main/antlr4/org/xembly/Xembly.g4 +++ b/src/main/antlr4/org/xembly/Xembly.g4 @@ -89,6 +89,15 @@ directive returns [Directive ret] } } | + 'XATTR' name=argument COMMA value=argument + { + try { + $ret = new XattrDirective($name.ret.toString(), $value.ret.toString()); + } catch (final XmlContentException ex) { + throw new ParsingException(ex); + } + } + | 'ADD' argument { try { diff --git a/src/main/java/org/xembly/Directives.java b/src/main/java/org/xembly/Directives.java index 2f461ac..6fd91af 100644 --- a/src/main/java/org/xembly/Directives.java +++ b/src/main/java/org/xembly/Directives.java @@ -348,7 +348,7 @@ public Directives attr(final Object name, final Object value) { } catch (final XmlContentException ex) { throw new IllegalArgumentException( String.format( - "failed to understand XML content, ATTR(%s, %s)", + "Failed to understand XML content, ATTR(%s, %s)", name, value ), ex @@ -377,7 +377,7 @@ public Directives pi(final Object target, final Object data) { } catch (final XmlContentException ex) { throw new IllegalArgumentException( String.format( - "failed to understand XML content, PI(%s, %s)", + "Failed to understand XML content, PI(%s, %s)", target, data ), ex @@ -403,7 +403,7 @@ public Directives set(final Object text) { } catch (final XmlContentException ex) { throw new IllegalArgumentException( String.format( - "failed to understand XML content, SET(%s)", + "Failed to understand XML content, SET(%s)", text ), ex @@ -424,7 +424,7 @@ public Directives xset(final Object text) { } catch (final XmlContentException ex) { throw new IllegalArgumentException( String.format( - "failed to understand XML content, XSET(%s)", + "Failed to understand XML content, XSET(%s)", text ), ex @@ -433,6 +433,28 @@ public Directives xset(final Object text) { return this; } + /** + * Set attribute. + * @param attr Attribute name + * @param text Text to set + * @return This object + * @since 0.28 + */ + public Directives xattr(final Object attr, final Object text) { + try { + this.all.add(new XattrDirective(attr.toString(), text.toString())); + } catch (final XmlContentException ex) { + throw new IllegalArgumentException( + String.format( + "Failed to understand XML content, XATTR(%s, %s)", + attr, text + ), + ex + ); + } + return this; + } + /** * Go one node/level up. * @return This object diff --git a/src/main/java/org/xembly/XattrDirective.java b/src/main/java/org/xembly/XattrDirective.java new file mode 100644 index 0000000..3579d99 --- /dev/null +++ b/src/main/java/org/xembly/XattrDirective.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2013-2022, xembly.org + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: 1) Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. 2) Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. 3) Neither the name of the xembly.org nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.xembly; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import lombok.EqualsAndHashCode; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * XATTR directive. + * + *

The class is immutable and thread-safe. + * + * @since 0.28 + */ +@EqualsAndHashCode(of = "expr") +final class XattrDirective implements Directive { + + /** + * XPath factory. + */ + private static final XPathFactory FACTORY = XPathFactory.newInstance(); + + /** + * Attribute name. + */ + private final transient Arg name; + + /** + * XPath to use. + */ + private final transient Arg expr; + + /** + * Public ctor. + * @param attr Name of the attr + * @param val Text value to set + * @throws XmlContentException If invalid input + */ + XattrDirective(final String attr, final String val) throws XmlContentException { + this.name = new Arg(attr); + this.expr = new Arg(val); + } + + @Override + public String toString() { + return String.format("XATTR %s, %s", this.name, this.expr); + } + + @Override + public Cursor exec(final Node dom, + final Cursor cursor, final Stack stack) + throws ImpossibleModificationException { + final ConcurrentMap values = + new ConcurrentHashMap<>(0); + final XPath xpath = XattrDirective.FACTORY.newXPath(); + for (final Node node : cursor) { + try { + values.put( + node, + xpath.evaluate( + this.expr.raw(), node, XPathConstants.STRING + ).toString() + ); + } catch (final XPathExpressionException ex) { + throw new ImpossibleModificationException( + String.format("Invalid XPath expr '%s'", this.expr), ex + ); + } + } + for (final Map.Entry entry : values.entrySet()) { + Element.class.cast(entry.getKey()).setAttribute( + this.name.raw(), entry.getValue() + ); + } + return cursor; + } + +} diff --git a/src/test/java/org/xembly/AttrDirectiveTest.java b/src/test/java/org/xembly/AttrDirectiveTest.java index c40b910..e553eeb 100644 --- a/src/test/java/org/xembly/AttrDirectiveTest.java +++ b/src/test/java/org/xembly/AttrDirectiveTest.java @@ -39,7 +39,7 @@ import org.w3c.dom.Element; /** - * Test case for {@link AddDirective}. + * Test case for {@link AttrDirective}. * * @since 0.1 */ diff --git a/src/test/java/org/xembly/XattrDirectiveTest.java b/src/test/java/org/xembly/XattrDirectiveTest.java new file mode 100644 index 0000000..08ff27d --- /dev/null +++ b/src/test/java/org/xembly/XattrDirectiveTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2013-2022, xembly.org + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: 1) Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. 2) Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. 3) Neither the name of the xembly.org nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.xembly; + +import com.jcabi.matchers.XhtmlMatchers; +import javax.xml.parsers.DocumentBuilderFactory; +import org.apache.commons.lang3.StringUtils; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; + +/** + * Test case for {@link XattrDirective}. + * + * @since 0.28 + */ +public final class XattrDirectiveTest { + + /** + * XattrDirective can set attributes to current nodes. + * @throws Exception If some problem inside + */ + @Test + public void setsAttributesToCurrentNodes() throws Exception { + final Iterable dirs = new Directives( + StringUtils.join( + new String[]{ + "ADD 'root'; ADD 'foo'; UP; ADD 'foo';", + "XPATH '//*'; XATTR 'bar', 'count(//foo)';", + } + ) + ); + final Document dom = DocumentBuilderFactory.newInstance() + .newDocumentBuilder().newDocument(); + new Xembler(dirs).apply(dom); + MatcherAssert.assertThat( + XhtmlMatchers.xhtml(dom), + XhtmlMatchers.hasXPaths("/root[@bar = 2]") + ); + } +}