tags:

views:

102

answers:

3

We use CruiseControl for our build server. It compiles our applications using MSBuild and uses its own XML logger that spits out something like the following XML:

<project name="CI">
  <target name="CompileApp">
    <project name="Project1.csproj">
      <target name="build">
        <error>Compilation error one!</error>
      </target>
      <target name="BeforeBuild">
        <project name="Project2.csproj">
          <target name="build">
            <error>Compilation error two!</error>
          </target>
        </project>
      </target>
    </project>
  </target>
</project>

I want to transform this into a report that outputs each project's errors. I don't want to report on the errors in other projects.

  Project "Project1.csproj": 1 error(s)
  Error(s):  
  Compilation error one!

  Project "Project2.csproj": 1 error(s)
  Error(s):
  Compilation error two!

This is the closest I've gotten, but it's not right. It doesn't filter out project2's errors when showing project1's errors.

<xsl:template>
  <xsl:variable select="//project[.//error]" name="projects.with.errors" />
  <xsl:apply-templates select="$projects.with.errors" />
</xsl:template>

<xsl:template match="project">
   <xsl:variable select="./*[not(project)]//error" name="errors" />
   <xsl:if test="count($errors) > 1">
      <!-- display errors -->
   </xsl:if>
</xsl:template>

How can I filter out any error nodes that have a different project ancestor than my current project node? I.e., how can I only select descendant error nodes that don't have a project ancestor?

The error nodes can have an arbitrary number of parent elements (usually , but not always).

+2  A: 

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>
 <xsl:strip-space elements="*"/>

 <xsl:key name="kProjectByName" match="project"
  use="@name"/>

 <xsl:key name="kErrByProject" match="error"
  use="ancestor::project[1]/@name"/>

 <xsl:template match="/">
   <xsl:for-each select=
    "//project
          [generate-id()
          =
           generate-id(key('kProjectByName', @name)[1])
           ]
    ">
     <xsl:variable name="vErrors" select=
       "key('kErrByProject', @name)"/>

     <xsl:if test="$vErrors">
         Project <xsl:value-of select="@name"/><xsl:text>: </xsl:text>
         <xsl:value-of select="count($vErrors)"/> errors.
         Errors:
         <xsl:for-each select="$vErrors">
           <xsl:value-of select="."/>
         </xsl:for-each>
         <xsl:text>&#xA;</xsl:text>
     </xsl:if>
   </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<project name="CI">
  <target name="CompileApp">
    <project name="Project1.csproj">
      <target name="build">
        <error>Compilation error one!</error>
      </target>
      <target name="BeforeBuild">
        <project name="Project2.csproj">
          <target name="build">
            <error>Compilation error two!</error>
          </target>
        </project>
      </target>
    </project>
  </target>
</project>

produces the wanted, correct result:

 Project Project1.csproj: 1 errors.
 Errors:
 Compilation error one!

 Project Project2.csproj: 1 errors.
 Errors:
 Compilation error two!
Dimitre Novatchev
@Dimitre: It's good, but complex, I think. Also, he has commented that *"@name (which may or may not be unique in my situation)"*. So, if you go with "first of a kind" `project` by `@name`, you could loose some deeper `project`.
Alejandro
@Alexandro: I am not missing any project name -- in fact the code of the template starts with finding all distinct project names.
Dimitre Novatchev
+1  A: 

Here's a quite simple solution:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output method="text" />

  <xsl:template match="/">
    <xsl:apply-templates select="//project[.//error]" />
  </xsl:template>

  <xsl:template match="project">
    <xsl:variable name="errors" select=".//error[ancestor::project[1]/@name = current()/@name]" />
    <xsl:if test="count($errors) != 0">
      <xsl:value-of select="concat('Project &quot;',@name,'&quot;: ',count($errors),' error(s)&#10;Error(s):&#10;')" />
      <xsl:apply-templates select="$errors" />
      <xsl:text>&#10;</xsl:text>
    </xsl:if>
  </xsl:template>

  <xsl:template match="error">
    <xsl:value-of select="concat(.,'&#10;')" />
  </xsl:template>
</xsl:stylesheet>

This stylesheet basically just calls a template for each project element anywhere in the document tree. This template stores a list of all errors in a variable, using an xpath that selects all error elements that are a descendant of the current project and have the current project element as it's first project ancestor. Then, if there are any, it just outputs the appropriate header text and applies a template to each error.

I'm using &#10; for the new line character here, but if you prefer the windows newline, you can use &#13;&#10;.

Minor caveat to this one; you'll end up with a blank line at the bottom of your output, as it adds one at the bottom of every project to separate it from the next.

Flynn1179
Instead of using @name (which may or may not be unique in my situation), I used generate-id(). My errors selector looks like this: <xsl:value-of select="//error[generate-id(ancestory::project[1]) = generate-id(current())" name="errors" />
splattered bits
Fair enough.. Given that it's project names, I was expecting they would be unique, but at least it's fairly easy to modify as required.
Flynn1179
This solution is remarkably inefficient. Expressions like: `//project[//error]` are good example of a XPath antipattern.The time complexity is O(N^2), where N is the number of elements in the XML document.
Dimitre Novatchev
@Flynn: If I'm a father, I know who are my childs.
Alejandro
FYI: *This solution is 3-4 times slower than mine* -- even for relatively small XML files (198 lines). *For an XML file with 982 lines it is 20-30 times slower*. For larger files it may be hundreds of times slower, because *its complexity is O(N^2) -- quadratical*, while mine is sub-linear. But I shouldn't be telling you this -- do your own timing... :)
Dimitre Novatchev
So what? I didn't say it was fast, I said it was simple. Speed's not always a priority, sometimes simpler and easier to maintain code is. Strictly speaking, you can leave off the `[//error]` predicate anyway, it would just mean the template would get called for all projects, but it would output nothing if there were no errors.
Flynn1179
I take one point though; it should really be `[.//error]`, to represent all descendents of `project` instead of all `error` elements in the document. I've amended it.
Flynn1179
Performance isn't too much of an issue for us. These get generated infrequently, and are then cached. Our builds take orders of magnitude longer than the report generation.I'll update my local files to use .//error, but I thought the . at the beginning was implied.
splattered bits
No, the . tells XPath to use the current node as a starting point, otherwise it starts from the document root.
Flynn1179
A: 

We are so used to complex requirements, we are puzzled when it appears a more simple, I think.

So, with:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output method="text" />

  <xsl:template match="project[../..]">
      <xsl:value-of select="concat('&#xA;',
            'Project &quot;',@name,'&quot;: ',count(target/error),' error(s)&#xA;',
              'Error(s):&#xA;')" />
      <xsl:apply-templates/>
  </xsl:template>

  <xsl:template match="error">
    <xsl:value-of select="concat(.,'&#xA;')" />
  </xsl:template>
</xsl:stylesheet>

You get:

Project "Project1.csproj": 1 error(s)
Error(s):
Compilation error one!

Project "Project2.csproj": 1 error(s)
Error(s):
Compilation error two!
Alejandro
You haven't paid attention to this comment of the original author:http://stackoverflow.com/questions/3135926/how-do-i-select-child-nodes-that-dont-have-a-parent-ancestor
Dimitre Novatchev
@Dimitre: He've provided input and output. So, I prefer to keep my own logic. In fact, the question being asked to solve their own implementation, is poorly formulated. Should be: "How do I select child nodes that don't have a **specific** parent ancestor"
Alejandro
I am not criticizing your solution. I just pointed out the fact that the OP clarified the problem with more specific information in his comment. I also find the title confusing.
Dimitre Novatchev
@Dimitre @Alejandro Totally agree the title is confusing. Now accepting applications for a new one! Suggestions? I just changed it to be "a specific parent as an ancestor" per Alejandro's suggestion.
splattered bits
@splattered bits: Good title! But I'm still convinced that this solutions is better. When you are `project`, you know you want all your `target/error` grandchilds. You don't need to filter all your `//error` descendant.
Alejandro