Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
172 views
in Technique[技术] by (71.8m points)

Attempting to use Java & Xpath to updated the value of any selected node inside an XML doc

I have developed GUI tool the displays an XML document as an editable JTree, and the user can select a node in the JTree and attempt to change the actual nodes value in the XML document.

The problem that I'm having is with constructing the correct Xpath query that attempts the actual update.

Here is GUI of the JTree showing which element was selected & should be edited:

enter image description here

Its a very large XMl, so here the collapsed snippet of the XML:

UPDATE (IGNORE ATTEMPT 1 & 2, 1ST ISSUE WAS RESOLVED, GO TO ATTEMPTS 3 & 4)

Attempt 1 # (relevant Java method that attempts to create XPath query to update a nodes value):

public void updateXmlData(JTree jTree, org.w3c.dom.Document doc, TreeNode parentNode, String oldValue, String newValue) throws XPathExpressionException {
        System.out.println("Selected path=" + jTree.getSelectionPath().toString());

        String[] pathTockens = jTree.getSelectionPath().toString().split(",");
        StringBuilder sb = new StringBuilder();
        //for loop to construct xpath query 
        for (int i = 0; i < pathTockens.length - 1; i++) {
            if (i == 0) {
                sb.append("//");
            } else {
                sb.append(pathTockens[i].trim());
                sb.append("/");
            }

        }//end for loop 
        sb.append("text()");
        System.out.println("Constructed XPath Query:" + sb.toString());
        //new xpath 
        XPath xpath = XPathFactory.newInstance().newXPath();
        //compile query 
        NodeList nodes = (NodeList) xpath.compile(sb.toString()).evaluate(doc, XPathConstants.NODESET);
        //Make the change on the selected nodes
        for (int idx = 0; idx < nodes.getLength(); idx++) {
            Node value = nodes.item(idx).getAttributes().getNamedItem("value");
            String val = value.getNodeValue();
            value.setNodeValue(val.replaceAll(oldValue, newValue));
        }
        //set the new updated xml doc 
        SingleTask.currentTask.setDoc(doc);
    }

Console logs:

Selected path=[C:UsersxyzDocumentsXsdToXmlFilessampleIngest.xml, Ingest, Property_Maps, identifier, identifieXYZ]
Constructed XPath Query://Ingest/Property_Maps/identifier/text()
Jan 26, 2021 2:04:16 PM com.xyz.XmlToXsdValidator.Views.EditXmlTreeNodeDialogJFrame jButtonOkEditActionPerformed
SEVERE: null

javax.xml.transform.TransformerException: Unable to evaluate expression using this context
    at com.sun.org.apache.xpath.internal.XPath.execute(XPath.java:368)

As you can see in the logs:

Selected path=[C:UsersxyzDocumentsXsdToXmlFilessampleIngest.xml, Ingest, Property_Maps, identifier, identifieXYZ]

Constructed XPath Query://Ingest/Property_Maps/identifier/text()

The paths are correct, basically Ingest->Property_Maps->identifier->text()

But Im getting:

 javax.xml.transform.TransformerException: Unable to evaluate expression using this context

Attempt 2 # (relevant Java method that attempts to create XPath query to update a nodes value):

public void updateXmlData(JTree jTree, org.w3c.dom.Document doc, TreeNode parentNode, String oldValue, String newValue) throws XPathExpressionException {
        // Locate the node(s) with xpath
        System.out.println("Selected path=" + jTree.getSelectionPath().toString());

        String[] pathTockens = jTree.getSelectionPath().toString().split(",");
        StringBuilder sb = new StringBuilder();
        //loop to construct xpath query 
        for (int i = 0; i < pathTockens.length - 1; i++) {
            if (i == 0) {
                sb.append("//");
            } else {
                sb.append(pathTockens[i].trim());
                sb.append("/");
            }
        }//end loop 

        sb.append("[text()=");
        sb.append("'");
        sb.append(oldValue);
        sb.append("']");
        int lastIndexOfPathChar = sb.lastIndexOf("/");
        sb.replace(lastIndexOfPathChar, lastIndexOfPathChar + 1, "");
        System.out.println("Constructed XPath Query:" + sb.toString());
        //new xpath instance 
        XPath xpath = XPathFactory.newInstance().newXPath();
        NodeList nodes = (NodeList) xpath.evaluate(sb.toString(), doc, XPathConstants.NODESET);

        //Make the change on the selected nodes
        for (int idx = 0; idx < nodes.getLength(); idx++) {
            Node value = nodes.item(idx).getAttributes().getNamedItem("value");
            String val = value.getNodeValue();
            value.setNodeValue(val.replaceAll(oldValue, newValue));
        }

        SingleTask.currentTask.setDoc(doc);
    }

I was able to resolve the exception based Andreas comment, and there are no more exceptions/errors, however the XPath query does not find selected nodes. Returns empty

New updated code:

Attempt # 3 Using custom namespace resolver. References: https://www.kdgregory.com/index.php?page=xml.xpath

   public boolean updateXmlData(JTree jTree, org.w3c.dom.Document doc, TreeNode parentNode, String oldValue, String newValue) throws XPathExpressionException {
            System.out.println("Selected path=" + jTree.getSelectionPath().toString());
            boolean changed = false;
            // Locate the node(s) with xpath
            String[] pathTockens = jTree.getSelectionPath().toString().split(",");
            StringBuilder sb = new StringBuilder();
            //loop to construct xpath query 
            for (int i = 0; i < pathTockens.length - 1; i++) {
    
                if (i == 0) {
                    //do nothing
                } else if (i == 1) {
                    sb.append("/ns:" + pathTockens[i].trim());
                } else if (i > 1 && i != pathTockens.length - 1) {
                    sb.append("/ns:" + pathTockens[i].trim());
                } else {
                    //sb.append("/" + pathTockens[i].trim());
                }
            }//end loop 
    
            sb.append("[text()=");
            sb.append("'");
            sb.append(oldValue);
            sb.append("']");
    
            System.out.println("Constructed XPath Query:" + sb.toString());
            //new xpath instance 
            XPathFactory xpathFactory = XPathFactory.newInstance(); 
            XPath xpath = xpathFactory.newXPath();
          
            xpath.setNamespaceContext(new UniversalNamespaceResolver(SingleTask.currentTask.getXsdFile().getXsdNameSpace()));
            NodeList nodes = (NodeList) xpath.evaluate(sb.toString(), doc, XPathConstants.NODESET);
    
            //start for 
            Node node;
            String val = null;
            for (int idx = 0; idx < nodes.getLength(); idx++) {
                if (nodes.item(idx).getAttributes() != null) {
    
                    node = nodes.item(idx).getAttributes().getNamedItem("value");
                    if (node != null) {
    
                        val = node.getNodeValue();
                        node.setNodeValue(val.replaceAll(oldValue, newValue));
                        changed = true;
                        break;
                    }//end if node is found
                }
            }//end for 
            //set the new updated xml doc 
            SingleTask.currentTask.setDoc(doc);
    
            return changed;
        }

Class that implements custom namespace resolver:

 import java.util.Arrays;
    import java.util.Collections;
    import java.util.Iterator;
    import java.util.List;
    import javax.xml.XMLConstants;
    import javax.xml.namespace.NamespaceContext;
    import org.w3c.dom.Document;
    
        /**
         *
         * References:https://www.kdgregory.com/index.php?page=xml.xpath
         */
        //custom  NamespaceContext clss implementation 
        public class UniversalNamespaceResolver implements NamespaceContext
        {
            private String _prefix = "ns";
            private String _namespaceUri=null; 
            private List<String> _prefixes = Arrays.asList(_prefix);
            
            public UniversalNamespaceResolver(String namespaceResolver)
            {
                _namespaceUri = namespaceResolver; 
            }
         
            @Override
            @SuppressWarnings("rawtypes")
            public Iterator getPrefixes(String uri)
            {
                if (uri == null)
                    throw new IllegalArgumentException("UniversalNamespaceResolver getPrefixes() URI may not be null");
                else if (_namespaceUri.equals(uri))
                    return _prefixes.iterator();
                else if (XMLConstants.XML_NS_URI.equals(uri))
                    return Arrays.asList(XMLConstants.XML_NS_PREFIX).iterator();
                else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(uri))
                    return Arrays.asList(XMLConstants.XMLNS_ATTRIBUTE).iterator();
                else
                    return Collections.emptyList().iterator();
            }
        
        
            @Override
            public String getPrefix(String uri)
            {
                if (uri == null)
                    throw new IllegalArgumentException("nsURI may not be null");
                else if (_namespaceUri.equals(uri))
                    return _prefix;
                else if (XMLConstants.XML_NS_URI.equals(uri))
                    return XMLConstants.XML_NS_PREFIX;
                else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(uri))
                    return XMLConstants.XMLNS_ATTRIBUTE;
                else
                    return null;
            }
        
        
            @Override
            public String getNamespaceURI(String prefix)
            {
                if (prefix == null)
                    throw new IllegalArgumentException("prefix may not be null");
                else if (_prefix.equals(prefix))
                    return _namespaceUri;
                else if (XMLConstants.XML_NS_PREFIX.equals(prefix))
                    return XMLConstants.XML_NS_URI;
                else if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix))
                    return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
                else
                    return null;
            }
        }

Console Output:

Selected path=[C:UsersxyzDocumentsIngest_LDD.xml, Ingest_LDD, Property_Maps, identifier, identifier1]

Constructed XPath: Query:/ns:Ingest_LDD/ns:Property_Maps/ns:identifier[text()='identifier1']

Attempt #4 (Without custom namespace resolver):

public boolean updateXmlData(JTree jTree, org.w3c.dom.Document doc, TreeNode parentNode, String oldValue, String newValue) throws XPathExpressio

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

For the test you performed online it seems your XML file contains namespace information.

With that information in mind, probably both of your examples of XPath evaluation would work, or not, dependent on several things.

For example, you probably can use the attempt #4, and the XPath evaluation will be adequate, if you are using a non namespace aware (the default) DocumentBuilderFactory and you o not provide any namespace information in your XPath expression.

But the XPath evaluation in attempt #3 can also be adequate if the inverse conditions apply, i.e., you are using a namespace aware DocumentBuilderFactory:

DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setNamespaceAware(true);

and you provide namespace information in your XPath expression and a convenient NamespaceContext implementation. Please, see this related SO question and this great IBM article.

Be aware that you do not need to provide the same namespace prefixes in both your XML file an XPath expression, the only requirement is namespace awareness in XML (XPath is always namespace aware).

Given that conditions, I think you can apply both approaches.

In any case, I think the problem may have to do with the way you are dealing with the actual text replacement: you are looking for a node with a value attribute, and reviewing the associated XML Schema this attribute does not exist.

Please, consider instead the following approach:

// You can get here following both attempts #3 an #4
Node node = (Node) xpath.evaluate(sb.toString(), doc, XPathConstants.NODE);
boolean changed = node != null;
if (changed) {
  // See https://docs.oracle.com/javase/9/docs/api/org/w3c/dom/Node.html#setTextContent-java.lang.String-
  node.setTextContent(newValue);
  SingleTask.currentTask.setDoc(doc);
}

return changed;

This code assumes that the selected node will be unique to work properly.

Although probably unlike, please, be aware that the way in which you are constructing the XPath selector from the JTree model can provide duplicates if you define the same value for repeated elements in your XML. Consider the elements external_id_property_maps in your screenshot, for instance.

In order to avoid that, you can take a different approach when constructing the XPath selector.

It is unclear for your code snippet, but probably you are using DefaultMutableTreeNode as the base JTree node type. If that is the case, you can associate with every node the arbitrary information you need to.

Consider for example the creation of a simple POJO with two fields, the name of the Element that the node represents, and some kind of unique, generated, id, let's name it uid or uuid to avoid confusion with the id attribute, most likely included in the original XML document.

This uid should be associated with every node. Maybe you can take advantage of the JTree creation process and, while processing every node of your XML file, include this attribute as well, generated using the UUID class, for example.

Or you can apply a XSLT transform to the original XML document prior to representation:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes"/>

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:attribute name="uid">
        <xsl:value-of select="generate-id(.)"/>
      </xsl:attribute>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

With this changes, your XPath query should looks like:

/ns:Ingest_LDD[@uid='w1ab1']/ns:Property_Maps[@uid='w1ab1a']/ns:identifier[@uid='w1ab1aq']

Of course, it will be necessary to modify the code devoted to the construction of this expression from the selected path of the JTree to take the custom object into account.

You can take this approach to the limit and use a single selector based solely in this uid attribute, although I think that for performance reasons it will be not appropriate:

//*[@uid='w1ab1']

Putting it all together, you can try something like the following.

Please, consider this XML file:

<?xml version="1.0" encoding="utf-8" ?>
<Ingest_LDD xmlns="http://pds.nasa.gov/pds4/pds/v1"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://pds.nasa.gov/pds4/pds/v1 https://pds.nasa.gov/pds4/pds/v1/PDS4_PDS_1700.xsd">
    <!-- Please, forgive me, I am aware that the document is not XML Schema conformant,
    only for exemplification of the default namespace -->
    <Property_Maps>
        <identifier>identifier1</identifier>
    </Property_Maps>
</Ingest_LDD>

First, let's parse the document:

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
// As the XML contains namespace, let's configure the parser namespace aware
// This will be of relevance when evaluating XPath
builderFactory.setNamespaceAware(true);
DocumentBuilder builder = builderFactory.newDocumentBuilder();
// Parse the document from some source
Document document = builder.parse(...);
// See http://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work
document.getDocumentElement().normalize();

Now, create a JTree structure corresponding to the input XML file. First, let's create a convenient POJO to store the required tree node information:

public class NodeInformation {
  // Node name
  private String name;
  // Node uid
  private String did;
  // Node value
  private String value;

  // Setters and getters

  // Will be reused by DefaultMutableTreeNode
  @Override
  public String toString() {
    return this.name;
  }
}

Convert the XML file to its JTree counterpart:

// Get a reference to root element
Element rootElement = document.getDocumentElement();
// Create root tree node
DefaultMutableTreeNode rootTreeNode = getNodeInformation(rootElement);
// Traverse DOM
traverse(rootTreeNode, rootElement);
// Create tree and tree model based on the computed root tree node
DefaultTreeModel treeModel = new DefaultTreeModel(rootTreeNode);
JTree tree = new JTree(treeModel);

Where:

private NodeInformation getNodeInformation(Node childElement) {
  NodeInformation nodeInformation = new NodeInformation();
  String name = childElement.getNodeName();
  nodeInformation.setName(name);
  // Provide a new unique identifier for every node
  String uid = UUID.randomUUID().toString();
  nodeInformation.setUid(uid);
  // Uhnn.... We need to associate the new uid with the DOM node as well. 
  // There is nothing wrong with it but mutating the DOM in this way in
  // a method that should be "read-only" is not the best solution.
  // It would be interesting to study the above-mentioned XSLT approach
  chilElement.setAttribute("uid", uid);

  // Compute node value
  StringBuffer buffer = new StringBuffer();
  NodeList childNodes = childElement.getChildNodes();
  boolean found = false;
  for (int i = 0; i < childNodes.getLength(); i++) {
    Node node = childNodes.item(i);
    if (node.getNodeType() == Node.TEXT_NODE) {
      String value = node.getNodeValue();
      buffer.append(value);
      found = true;
    }
  }

  if (found) {
    nodeInformation.setValue(buffer.toString());
  }
}

And:

// Finds all the child elements and adds them to the parent node recursively
private void traverse(DefaultMutableTreeNode parentTreeNode, Node parentXMLElement) {
  NodeList childElements = parentXMLElement.getChildNodes();
  for(int i=0; i<childElements.getLength(); i++) {
    Node childElement = childElements.item(i);
    if (childElement.getNodeType() == Node.ELEMENT_NODE) {
      DefaultMutableTreeNode childTreeNode =
        new DefaultMutableTreeNode
          (getNodeInformation(childElement));
      parentTreeNode.add(childTreeNode);
      traverse(childTreeNode, childElement);
    }
  }
}

Although the NamespaceContext implementation you provided looks fine, please, at a first step, try something simpler, to minimize the possibility of error. See the provided implementation below.

Then, your updateXMLData method should looks like:

public boolean updateXmlData(JTree tree, org.w3c.dom.Document doc, TreeNode parentNode, String oldValue, String newValue) throws XPathExpressionException {
  boolean changed = false;

  TreePath selectedPath = tree.getSelectionPath();
  int count = getPathCount();
  StringBuilder sb = new StringBuilder();
  NodeInformation lastNodeInformation;
  if (count > 0) {
    for (int i = 1; i < trp.getPathCount(); i++) {
      DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) trp.getPathComponent(i);
      NodeInformation nodeInformation = (NodeInformation) treeNode.getUserObject();
      sb.append(String.format("/ns:%s[@uid='%s']", nodeInformation.getName(), nodeInformation.getUid());
      lastNodeInformation = nodeInformation;
    }
  }

  System.out.println("Constructed XPath Query:" + sb.toString());

  // Although the `NamespaceContext` implementation you provided looks
  // fine, please, at a first step, try something simpler, to minimize the
  // possibility of error. For example:
  NamespaceContext nsContext = new NamespaceContext() {

    public String getNamespaceURI(String prefix) {
      if (prefix == null) {
        throw new IllegalArgumentException("No prefix provided!");
      } else if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
        return "http://pds.nasa.gov/pds4/pds/v1";
      } else if (prefix.equals("ns")) {
        return "http://pds.nasa.gov/pds4/pds/v1";
      } else {
        return XMLConstants.NULL_NS_URI;
      }
    }

    public String getPrefix(String namespaceURI) {
      // Not needed in this context.
      return null;
    }

    public Iterator getPrefixes(String namespaceURI) {
      // Not needed in this context.
      return null;
    }

  };

  //new xpath instance 
  XPat

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...