tags:

views:

373

answers:

6

Hi,

I have a buggy xml that contains empty attributes and I have a parser that coughs on empty attributes. I have no control over the generation of the xml nor over the parser that coughs on empty attrs. So what I want to do is a pre-processing step that simply removes all empty attributes.

I have managed to find the empty attributes, but now I don't know how to remove them:

   XPathFactory xpf = XPathFactory.newInstance();
   XPath xpath = xpf.newXPath();
   XPathExpression expr = xpath.compile("//@*");
   Object result = expr.evaluate(d, XPathConstants.NODESET);

   if (result != null) {
    NodeList nodes = (NodeList) result;
    for(int node=0;node<nodes.getLength();node++)
    {
     Node n = nodes.item(node);
     if(isEmpty(n.getTextContent()))
     {
      this.log.warn("Found empty attribute declaration "+n.toString());
      NamedNodeMap parentAttrs = n.getParentNode().getAttributes();
      parentAttrs.removeNamedItem(n.getNodeName());
     }
    }

   } 

This code gives me a NPE when accessing n.getParentNode().getAttributes(). But how can I remove the empty attribute from an element, when I cannot access the element?

A: 

getParentNode() doesn't work on attributes.

All nodes, except Attr, Document, DocumentFragment, Entity, and Notation may have a parent.

not 100% sure, but i think you can select all nodes that have an attribute with the following expression:

//*[@*]

you can then easily loop over the attributes and check if they are empty

Stefan De Boey
Thanks. Setteld with this one just a minute ago ;-).
er4z0r
+1  A: 

This is probably not the way to do it anyway. Removing something from your NodeList is not going to remove it from the XML. If your parser is actually processing an already loaded DOM and you're manipulating the DOM before the parser gets it something similar to this might work, but it's likely not the best tactic.

You're probably better off preprocessing it by passing it through an XMLFilter on its way to the parser. I located a IBM Developerworks article with sample code that removes all attributes, and it's part of a series that earlier shows how to hook up a chain of filters to your parser.

All this assumes that you're using a SAX parser, but if it's something else, there are likely ways of using SAX and such a filter in a preprocessing step of some sort.

It's also possible you can do the preprocessing by xslt.

Don Roby
A: 

I would check to make sure that you are actually receiving lists of just nodes of type ATTR, and not Elements, or a mix of the two. I have not used XPathExpression however it may interpret the path "//@*" as "any element with an attribute" as opposed to "all attributes" (what I expect you mean). If the former is true, and your root node has an attribute, it would appear in the resultant nodelist from the query, and by definition [root node].getParentNode() == null producing your NPE.

In addition if you are selecting element nodes and not attr nodes with your query the expression n.getTextContent() would be looking at the text content, not an attribute value (again a likely cause leading to your NPE if the root node is in the list, since most root nodes don't have text content), additionally the attempted removal of the attribute would be a no-op (which you don't really intend anyway).

So if you are receiving element nodes instead of attribute nodes, then you should look at the attribute map and then modify it, and if you have to look at all the attributes, you may be better off just writing a Depth-First-Search looking at the DOM and performing the modifications there.

M. Jessup
A: 

I actually found a way to do it. Allthough this will not solve the problem perfectly it is O.K. for now. In case of using this, be warned, that it will only catch atributes that have a value that is exactly '' other nonsense like a value only consisting of whitespace will not be caught by this.

   XPathFactory xpf = XPathFactory.newInstance();
   XPath xpath = xpf.newXPath();
   XPathExpression expr = xpath.compile("//*[@*='']");
   Object result = expr.evaluate(d, XPathConstants.NODESET);

   if (result != null) {
    NodeList nodes = (NodeList) result;
    for(int node=0;node<nodes.getLength();node++)
    {
     Node n = nodes.item(node);
     NamedNodeMap attrs = n.getAttributes();
     for(int attr=0;attr<attrs.getLength();attr++)
     {
      Node a = attrs.item(attr);
      if(isEmpty(a.getNodeValue()));
      {
       attrs.removeNamedItem(a.getNodeName());
       this.log.warn("Removing empty attribute "+a.toString()+" from element "+n.getNodeName());
      }
     }
    }

   } 

What a pity regex for comparison is only available as an XSLT extension and not granted to be supported on every XSLT-Processor :-(

er4z0r
However, EXSL is widely supported, and includes regex support. And if the alternative is to limit yourself to a java implementation anyhow, then it's not like you're losing any portability anyhow...
Eamon Nerbonne
+2  A: 

If you want to limit it to just the empty attributes, you can use this XPATH:

//*[@*[.='']]

To find attributes that are either empty or that have only whitespace:

//*[@*[normalize-space()='']].

That way you select the attributes you want to remove and don't have to loop over every single attribute just to find the empty ones.

Mads Hansen
I think you have a typo in the second expression. At least my XSLT-Processor complains about `expecting ] found .'
er4z0r
@er4z0r - whoops! I had left the `.` when I added `normalize-space()` for the second expression. I've corrected the answer. FYI - I could have left it in if I had put it inside the `normalize-space()` function (i.e. `normalize-space(.)` ). Either way works.
Mads Hansen
Thanks. Found it out by checking the xpath functions documentation ;-)
er4z0r
A: 

The following stylesheet will copy all content in the source document - except attributes that contain only whitespace. The first template simply copies everything - including empty attributes. However, the second template has a higher priority than the first due to its use of a predicate, which is why it will be chosen in preference to the more general first template when an empty attribute is encountered: and this second template does not generate any output.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> 
<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>
<xsl:template match="@*[normalize-space()='']"/>
</xsl:template>
Eamon Nerbonne
I'm just starting to get a grasp of XSLT. Can you tell me why your identity-transform (that's what your copy-everything is usually called, right?) does not have a select="@*|node()|text()"
er4z0r
I can answer that question with a quote from the standard (http://www.w3.org/TR/xslt#patterns): "node() matches any node other than an attribute node and the root node". We don't care about the root node `/` but do care about attributes - hence the `@*`.
Eamon Nerbonne
Reading the spec reveals fun facts: turns out the spec has an example copy-template, and it's literally identical to the above: http://www.w3.org/TR/xslt#copying
Eamon Nerbonne
THanks your your help. I know I should read the spec, but its kinda lengthy.
er4z0r
Don't bother - I use zvon which is more readable 99% of the time - and it links to the appropriate section of the spec for when you *really* need the details. (The CSS spec, however, is actually quite comprehensible and much more precise than many web-guides, that's actually worth looking at quite often).
Eamon Nerbonne
Incidentally, if there's one bit of the spec that's worth taking a peek at it's the priority rules; if ever you can't figure out why a particular template is chosen, that's where you'll find out why...
Eamon Nerbonne