raven-king
raven-king

Reputation: 1550

Copying referenced XML elements

I have a source XML document which looks somthing like this:

<root>
    <users>
        <user id="1" name="user 1" />
        <user id="2" name="user 2" />
        <user id="3" name="user 3" />
    </users>
    <posts>
        <post>
            <user>1</user>
            <text>First sample post!</text>
            <status>DELETED</status>
        </post>
        <post>
            <user>2</user>
            <text>Second sample post!</text>
            <status>ACTIVE</status>
        </post>
        <post>
            <user>3</user>
            <text>Third sample post!</text>
            <status>DELETED</status>
        </post>
    </posts>
</root>

I need to filter the users so that the target document contains only ACTIVE posts and those users refered to in the post element.:

<root>
    <users>
        <user id="2" name="user 2" />
    </users>
    <posts>
        <post>
            <user>2</user>
            <text>Second sample post!</text>
        </post>
    </posts>
</root>

I don't have access to change the source document and I'm required to make this possible using XSLT (which I'm very new to).

I can filter the posts easily but I'm not sure how to build up the list of users.

Before I go too much futher I'd like to check if this is possible.

Cheers

Upvotes: 0

Views: 46

Answers (2)

Tim C
Tim C

Reputation: 70618

First you should learn about the Identity Template

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

On its own it will copy across all nodes exactly from the source document.

This means, rather than thinking in terms of what you need to copy, think in terms what you don't need to copy. This is achieved by adding templates, with higher priority, that override the identity template.

You don't want post elements where status is not "ACTIVE"? Just have an empty template to stop them being copied.

<xsl:template match="post[status!='ACTIVE']" />

Similarly, for removing the status node itself (for the posts it does copy)

<xsl:template match="status" />

For your user elements, consider using an xsl:key to look up post elements

<xsl:key name="posts" match="post" use="user" />

Then, your template to ignore users would be this....

<xsl:template match="user[key('posts', @id)/status!='ACTIVE']" />

Put this all together gives you this...

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" indent="yes" />
    <xsl:strip-space elements="*" />

    <xsl:key name="posts" match="post" use="user" />

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="post[status!='ACTIVE']" />

    <xsl:template match="status" />

    <xsl:template match="user[key('posts', @id)/status!='ACTIVE']" />
</xsl:stylesheet>

Upvotes: 1

Andreas
Andreas

Reputation: 159114

Yes, it is possible, with a stylesheet like this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <!-- Copy asnything not overridden below -->
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- Copy <user> if active <post> exists -->
    <xsl:template match="users/user"><!-- don't match 'post/user' -->
        <xsl:variable name="userId" select="@id"/>
        <xsl:if test="../../posts/post[user = $userId][status = 'ACTIVE']">
            <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
            </xsl:copy>
        </xsl:if>
    </xsl:template>

    <!-- Copy <post> if active -->
    <xsl:template match="post">
        <xsl:if test="status = 'ACTIVE'">
            <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
            </xsl:copy>
        </xsl:if>
    </xsl:template>

    <!-- Don't copy <status> -->
    <xsl:template match="status">
    </xsl:template>

</xsl:stylesheet>

Test

TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer(new StreamSource(new File("test.xslt")));
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new StreamSource(new File("test.xml")),
                      new StreamResult(System.out));

Output

<root>
    <users>

        <user id="2" name="user 2"/>

    </users>
    <posts>

        <post>
            <user>2</user>
            <text>Second sample post!</text>

        </post>

    </posts>
</root>

Upvotes: 1

Related Questions