3

I am trying to add child element to a node in XML using XML:Twig. my XML is

 <Install>
<version >
<number>6.0</number>
<build>1037124</build>
<path>path</path>
<kind>kind</kind>
</version>
<version >
<number>7.0</number>
<build>1037124</build>
<path>path</path>
<kind>kind</kind>
</version>
</Install>

and the output I want is:-

 <Install>
<version >
<number>6.0</number>
<build>1037124</build>
<path>path/path>
<kind>kind</kind>
<patch>patch</patch>
<patchv>patchv</patchv>  
</version>
<version >
<number>7.0</number>
<build>1037124</build>
<path>path</path>
<kind>kind</kind>
</version>
</Install>

I want to add two new child node to parent version where number is 6.0

I have tried this code

    my $version = "6.0";    
    my $file_loc = "data.xml";
    my $twig = XML::Twig->new( pretty_print => 'indented_a' )->parsefile( $file_loc );

for my $number ( $twig->findnodes('/Install/version/number') ) {
  $number->parent->last_child->insert(
              patch => {  version =>'patch' },
              patchversion => {  version =>'patchv' },
        ) if $number->trimmed_text eq $version;
}

but the output I am getting is not what I want. output is:-

<Install>
  <version>
    <number>6.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>
      <patch version="patch">
    <patchversion version="patchv">kind</patchversion>
      </patch>
    </kind>
  </version>
     <version >
    <number>7.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>kind</kind>
    </version>
    </Install>

I have tried this with other way also using paste as follows:-

    my $version = "6.0";    
    my $file_loc = "data.xml";


        my $myXML = <<"XML";
<patch>
<build></build>
<version></version>
</patch>
XML
my $t = XML::Twig->new(
  twig_handlers => {
    Install => sub { add_version( $myXML, @_ ); }, }
  )->parsefile( $file_loc ) or die $!;

$t->set_pretty_print('indented_c');
open my $xml_fh, '>', "$file_loc" or die $!;
$t->print($xml_fh);
sub add_version {
    my ( $xml, $t, $install ) = @_;
    my $new_elt = XML::Twig::Elt->parse($xml);
    for my $number ( $t->findnodes('/Install/version/number') ) {
    if($number->trimmed_text eq $version )
    {
    $new_elt->paste(  $number->parent->last_child->paste => $install );
    }
    }
}

here I am getting following error

cannot paste an element that belongs to a tree at

can I achieve this using insert only or I have to use paste?? How to do it in either case ??

5
  • Your XML is not well-formed. There is a < missing ni your input. While the overall question is very good, please still make sure you post valid code and XML (unless the question is "why does this not compile"). Thanks. :) Commented Aug 21, 2015 at 9:24
  • @simbabque I missed "<" possibly while writing this question, my original file is fine :) still looking for the answer though I have tried many things none worked. Commented Aug 21, 2015 at 9:41
  • Do you want me to explain why your second attempt doesn't do what you want as well? Commented Aug 21, 2015 at 9:41
  • yes you are welcomed :) Commented Aug 21, 2015 at 9:43
  • I added an explanation for the second approach. Commented Aug 21, 2015 at 10:01

1 Answer 1

2

You were on the right track with your first attempt, but you need to use insert_new_element. insert replaces the content of an element, and will create attributes with the syntax you chose.

You can put it all in a very simple twig_handler. Grab all the <version> elements, checkt their <number>'s text node and then add a new element with a text node inside for both the patch and patchv values.

The last_child => '' part puts the new element at the end of the parent. It is the $optional_position that is documented with paste.

use strict;
use warnings;
use XML::Twig;

my $twig = XML::Twig->new(
    pretty_print  => 'indented_a',
    twig_handlers => {
        'version' => sub {
            if ( $_->first_child('number')->text() eq '6.0' ) {
                $_->insert_new_elt( last_child => 'patch' )->set_text('patch');
                $_->insert_new_elt( last_child => 'patchv' )->set_text('patchv');
            }
        }
    }
)->parse( \*DATA );

$twig->print;

Output:

<Install>
  <version>
    <number>6.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>kind</kind>
    <patch>patch</patch>
    <patchv>patchv</patchv>
  </version>
  <version>
    <number>7.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>kind</kind>
  </version>
</Install>

Your second attempt does not work for at least two reasons:

# this elem get's pasted (1)
# |               on this positon (2)                            
# |               |                                     in that elem (3)
$new_elt->paste(  $number->parent->last_child->paste => $install );

In (2) it should have the position, which is a string. That's something like first_child, last_child and so on. They are listed in the docs of paste.

So it would compute the return value of $number->parent->last_child->paste and use that as the position string. But there is no such return value, so that will not work.

But it doesn't get that far. The error message cannot paste an element that belongs to a tree comes from the paste you have in (2), because you are calling paste() on $number->parent->last_child and that is already in a tree. That's not allowed. If you remove that part the code compiles.

if ( $number->trimmed_text eq $version ) {
  $new_elt->paste( $install );
}

The output is (snipped):

<Install>
  <patch>
    <build></build>
    <version></version>
  </patch>
  <version>
    <number>6.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>kind</kind>
  </version>

That is still not what we want because <patch> ended up inside <Install>, not inside <version>. But we already have $number from the loop. That is the <number> node inside of the <version> node, so we can get its parent and use that in our (3).

$new_elt->paste( $number->parent );

Now we get this:

<Install>
  <version>
    <patch>
      <build></build>
      <version></version>
    </patch>
    <number>6.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>kind</kind>
  </version>

That's better. It's kind of in the right place. If you add the position last_child it ends up at the end.

$new_elt->paste( last_child => $number->parent );

Now it's here:

<Install>
  <version>
    <number>6.0</number>
    <build>1037124</build>
    <path>path</path>
    <kind>kind</kind>
    <patch>
      <build></build>
      <version></version>
    </patch>
  </version>

Now it just is the wrong stuff we are pasting, because we wanted two elements <patch> and <patchv>, both with text. That's because you have those in $myXML.

You cannot have two top-level elements there. That's invalid XML syntax.

my $myXML = <<"XML";
<patch>patch</patch>
<patchv>patchv</patchv>
XML

That will fail with junk after document element at line 2, column 0, byte 21 at .... So you would have to either have two variables and call paste twice, or wrap them and fetch the child-nodes of the new root element.

But of course that is very complicated. The first approach above is way cleaner.

Sign up to request clarification or add additional context in comments.

4 Comments

thanks for the answer, can you explain why second approach doesn't worked.
Also for your approach of the wrapped add_version( $myXML, @_) check out curry, that makes it easier. I explained that in an answer here. But I still think you don't need it in this situation.
yes I faced the both problem you mentioned above that's why I changed my Input xml. Thanks For the nice and easy explanation :)
@user3649361 you're welcome. I believe there is a lot more value in an explanation of why something doesn't work than in just providing a solution. The explanation helps to think outside of the box in my opinion. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.