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
|