File: Xml.pm

package info (click to toggle)
libur-perl 0.470%2Bds-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 7,184 kB
  • sloc: perl: 61,813; javascript: 255; xml: 108; sh: 13; makefile: 9
file content (301 lines) | stat: -rw-r--r-- 10,087 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
package UR::Object::View::Default::Xml;

use strict;
use warnings;
require UR;
our $VERSION = "0.47"; # UR $VERSION;
use IO::File;
use XML::Dumper;
use XML::LibXML;

class UR::Object::View::Default::Xml {
    is => 'UR::Object::View::Default::Text',
    has_constant => [
        toolkit     => { value => 'xml' },
    ],
    has_optional_transient => [
        _xml_doc    => { is => 'XML::LibXML::Document', doc => 'The LibXML document used to create the content for this view', },
    ],
};

sub xsl_template_files {
    my $self = shift;  #usually this is a view without a subject attached
    my $output_format = shift;
    my $root_path = shift;
    my $perspective = shift || lc($self->perspective);

    my @xsl_names = map {
       $_ =~ s/::/_/g;
       my $pf = "/$output_format/$perspective/" . lc($_) . '.xsl';
       my $df = "/$output_format/default/" . lc($_) . '.xsl';

       -e $root_path . $pf ? $pf : (-e $root_path . $df ? $df : undef)
    } $self->all_subject_classes_ancestry;

    my @found_xsl_names = grep {
        defined
    } @xsl_names;

    return @found_xsl_names;
}

sub _generate_xml_doc {
    my $self = shift;

    my $subject = $self->subject();
    return unless $subject;

    my $xml_doc = XML::LibXML->createDocument();
    $self->_xml_doc($xml_doc);

    # the header line is the class followed by the id
    my $object = $xml_doc->createElement('object');
    $xml_doc->setDocumentElement($object);

    $object->addChild( $xml_doc->createAttribute('type', $self->subject_class_name) );

    $object->addChild( $xml_doc->createAttribute('id', $subject->id ) );

    my $display_name = $object->addChild( $xml_doc->createElement('display_name') );
    $display_name->addChild( $xml_doc->createTextNode($subject->__display_name__) );

    my $label_name = $object->addChild( $xml_doc->createElement('label_name' ));
    $label_name->addChild( $xml_doc->createTextNode($subject->__label_name__) );

    my $types = $object->addChild( $xml_doc->createElement('types') );
    foreach my $c ($self->subject_class_name,$subject->__meta__->ancestry_class_names) {
        my $isa = $types->addChild( $xml_doc->createElement('isa') );
        $isa->addChild( $xml_doc->createAttribute('type', $c) );
    }

    unless ($self->_subject_is_used_in_an_encompassing_view()) {
        # the content for any given aspect is handled separately
        my @aspects = $self->aspects;
        if (@aspects) {
            my @sorted_aspects = map { $_->[1] }
                                 sort { $a->[0] <=> $b->[0] }
                                 map { [ $_->number, $_ ] }
                                 @aspects;
            for my $aspect (@sorted_aspects) {
                next if $aspect->name eq 'id';

                my $aspect_node = $self->_generate_content_for_aspect($aspect);

                $object->addChild( $aspect_node ) if $aspect_node; #If aspect has no values, it won't be included
            }
        }
    }

    #From the XML::LibXML documentation:
    #If $format is 1, libxml2 will add ignorable white spaces, so the nodes content is easier to read. Existing text nodes will not be altered
    #If $format is 2 (or higher), libxml2 will act as $format == 1 but it add a leading and a trailing line break to each text node.

    return $xml_doc;
}

sub _generate_content {
    my $self = shift;

    my $xml_doc = $self->_generate_xml_doc;
    return '' unless $xml_doc;

    my $doc_string = $xml_doc->toString(1);

    # remove invalid XML entities
    $doc_string =~ s/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]//go;

    return $doc_string;
}

sub _add_perl_data_to_node {
    my $self = shift;
    my $perlref = shift;
    my $node = shift;

    my $xml_doc = $self->_xml_doc;
    $node ||= $xml_doc->documentElement;

    my $d = XML::Dumper->new;
    my $perldata = $d->pl2xml($perlref);

    my $parser = XML::LibXML->new;
    my $ref_xml_doc = $parser->parse_string($perldata);
    my $ref_root = $ref_xml_doc->documentElement;
    $xml_doc->adoptNode( $ref_root );
    $node->addChild( $ref_root );

    return 1;
}

sub _generate_content_for_aspect {
    # This does two odd things:
    # 1. It gets the value(s) for an aspect, then expects to just print them
    #    unless there is a delegate view.  In which case, it replaces them
    #    with the delegate's content.
    # 2. In cases where more than one value is returned, it recycles the same
    #    view and keeps the content.
    #
    # These shortcuts make it hard to abstract out logic from toolkit-specifics

    my $self = shift;
    my $aspect = shift;

    my $subject = $self->subject;
    my $xml_doc = $self->_xml_doc;
    my $aspect_name = $aspect->name;

    my $aspect_node = $xml_doc->createElement('aspect');
    $aspect_node->addChild( $xml_doc->createAttribute('name', $aspect_name) );

    my @value;
    eval {
        @value = $subject->$aspect_name;
    };
    if ($@) {
        my ($file,$line) = ($@ =~ /at (.*?) line (\d+)$/m);

        my $exception = $aspect_node->addChild( $xml_doc->createElement('exception') );
        $exception->addChild( $xml_doc->createAttribute('file', $file) );
        $exception->addChild( $xml_doc->createAttribute('line', $line) );
        $exception->addChild( $xml_doc->createCDATASection($@) );

        return $aspect_node;
    }

    if (not Scalar::Util::blessed($value[0])) {
        # shortcut to optimize for simple scalar values without delegate views
        for my $value ( @value ) {
            my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') );
            $value = '' if not defined $value;
            $value_node->addChild( $xml_doc->createTextNode($value) );
        }
        return $aspect_node;
    }

    unless ($aspect->delegate_view) {
        $aspect->generate_delegate_view;
    }

    # Delegate to a subordinate view if needed.
    # This means we replace the value(s) with their
    # subordinate widget content.
    my $delegate_view = $aspect->delegate_view;
    unless ($delegate_view) {
        Carp::confess("No delegate view???");
    }

    foreach my $value ( @value ) {
        if (Scalar::Util::blessed($value)) {
            $delegate_view->subject($value);
        } else {
            $delegate_view->subject_id($value);
        }
        $delegate_view->_update_view_from_subject();

        # merge the delegate view's XML into this one
        if ($delegate_view->can('_xml_doc') and $delegate_view->_xml_doc) {
            # the delegate has XML
            my $delegate_xml_doc = $delegate_view->_xml_doc;
            my $delegate_root = $delegate_xml_doc->documentElement;
            #cloneNode($deep = 1)
            $aspect_node->addChild( $delegate_root->cloneNode(1) );
        }
        elsif (ref($value) and not $value->isa("UR::Value")) {
            # Note: Let UR::Values display content below
            # Otherwise, the delegate view has no XML object, and the value is a reference
            $self->_add_perl_data_to_node($value, $aspect_node);
        }
        elsif (ref($value) and $value->isa("UR::Value")) {
            # For a UR::Value return both a formatted value and a raw value.
            my $display_value_node = $aspect_node->addChild( $xml_doc->createElement('display_value') );
            my $content = $delegate_view->content;
            $content = '' if not defined $content;
            $display_value_node->addChild( $xml_doc->createTextNode($content) );

            my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') );
            $content = $value->id;
            $value_node->addChild( $xml_doc->createTextNode($content) );
        }
        else {
            # no delegate view has no XML object, and the value is a non-reference
            # (this is the old logic for non-delegate views when we didn't have delegate views for primitives)
            my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') );
            unless(defined $value) {
                $value = '';
            }
            my $content = $delegate_view->content;
            $content = '' if not defined $content;
            $value_node->addChild( $xml_doc->createTextNode($content) );
            
            ## old logic for delegate views with no xml doc (unused now) 
            ## the delegate view may not be XML at all--wrap it in our aspect tag so that it parses
            ## (assuming that whatever delegate was selected properly escapes anything that needs escaping)

            # my $delegate_text = $delegate_view->content() ? $delegate_view->content() : '';
            # my $aspect_text = "<aspect name=\"$aspect_name\">\n$delegate_text\n</aspect>";
            # my $parser = XML::LibXML->new;
            # my $delegate_xml_doc = $parser->parse_string($aspect_text);
            # $aspect_node = $delegate_xml_doc->documentElement;
            # $xml_doc->adoptNode( $aspect_node );
        }
    }

    return $aspect_node;
}

# Do not return any aspects by default if we're embedded in another view
# The creator of the view will have to specify them manually
sub _resolve_default_aspects {
    my $self = shift;
    unless ($self->parent_view) {
        return $self->SUPER::_resolve_default_aspects;
    }
    return;
}

1;


=pod

=head1 NAME

UR::Object::View::Default::Xml - represent object state in XML format

=head1 SYNOPSIS

  $o = Acme::Product->get(1234);

  $v = $o->create_view(
      toolkit => 'xml',
      aspects => [
        'id',
        'name',
        'qty_on_hand',
        'outstanding_orders' => [
          'id',
          'status',
          'customer' => [
            'id',
            'name',
          ]
        ],
      ],
  );

  $xml1 = $v->content;

  $o->qty_on_hand(200);

  $xml2 = $v->content;

=head1 DESCRIPTION

This class implements basic XML views of objects.  It has standard behavior for all text views.

=head1 SEE ALSO

UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object

=cut