package ebXML::Message; =head1 NAME ebXML::Message - encapsulate an ebMS message =head1 SYNOPSIS use ebXML::Message; # create new message my $message = ebXML::Message->new ( 'CPAId' => 'http://www.you.org/cpa/123456', 'Action' => 'NewPurchaseOrder', 'MessageId' => 12, 'RefMessageToId' => 11, 'Service' => ebXML::Message::Service->new ( VALUE => 'QuoteToCollect', Type => 'myservicetypes', ), ); # write SOAP XML using DOM use XML::Xerses; # DOM Document my $target = XML::Xerces::StdOutFormatTarget->new(); $writer->writeNode($target,$message->getOutput('DOM-Doc'); # DOM generated / munged XML print OUT $message->getOutput('DOM-XML'); # use message with SOAP::Lite to call webservice use SOAP::Lite; # SOAP::Data::Builder object my $result = SOAP::Lite->uri('http://uri.to/WebService') ->proxy('http://uri.to/soap.cgi') ->parts($message->getMIMEParts) ->call($message->getOutput('SOAP-Data')) ->result; # SOAP::Data::Builder generated / munged XML print OUT $message->getOutput('SOAP-XML'); =head1 DESCRIPTION An ebXML message encapsulates all the details of an ebMS message. ebXML is a mechanism for ensuring reliable delivery of XML-based messages via a transport mechanism such as SOAP. For more details on ebXML, see http://www.ebxml.org/ Large portions of this manual page are copied directly from the ebMS 2.0 specification. =cut use base qw(Class::Tangram); our $VERSION = 0.03; =head1 PROPERTIES An ebMS message contains the following properties (case sensitive): =over =item B and B The REQUIRED B property identifies the Party that originated the message. The REQUIRED B property identifies the Party that is the intended recipient of the message. Both B and B can contain logical identifiers, such as a DUNS number, or identifiers that also imply a physical location such as an eMail address. The B and the B properties are references to B objects. These objects have: =over =item a list of Bs which occurs one or more times. The B property has a B and content that is a string value. The B indicates the domain of names to which the string in the content of the B element belongs. The value of the B MUST be mutually agreed and understood by each of the Parties. It is RECOMMENDED that the value of the B be a URI. It is further recommended that these values be taken from the EDIRA (ISO 6523), EDIFACT ISO 9735 or ANSI ASC X12 I05 registries. If the B B is B, the content of the B element MUST be a URI [RFC2396], otherwise the I SHOULD report an error (see section 11) with B set to B and B set to B. It is strongly RECOMMENDED that the content of each B be a URI. Cget_PartyIds()> returns a list of B =E B pairs. Cset_PartyIds()> accepts the same input. =item a list of Bs which occurs zero or one times The B property identifies the authorized role (fromAuthorizedRole or toAuthorizedRole) of the Party I (when present as a child of the B element) and/or I (when present as a child of the B element) the message. The value of each element of the B is a non-empty string, which is specified in the CPA. =back If either the B or B properties contain multiple B elements, all members of the list MUST identify the same organisation. Unless a single B value refers to multiple identification systems, a B attribute value must not appear more than once in a single list of B elements. Note: This mechanism is particularly useful when transport of a message between the parties may involve multiple intermediaries (see Sections 8.5.4, Multi-hop TraceHeader Sample and 10.3, ebXML Reliable Messaging Protocol). More generally, the From Party should provide identification in all domains it knows in support of intermediaries and destinations that may give preference to particular identification systems. The B and B elements contain zero or one B child element that, if present, SHALL immediately follow the last B child element. The following fragment demonstrates usage of the B and B elements. 123456789 RDWY mailto:president.brown@california.uber.alles This is set in a B object via the following Perl fragment: $message->set_From ( [ PartyIds => [ 'urn:duns' => "123456789", 'SCAC' => "RDWY", ], Roles => [ "X-Originator" ], ] ); $message->set_To ( [ PartyIds => [ undef => 'mailto:president.brown@california.uber.alles' ], Roles => [ "X-Recipient" ], ] ); =item B The REQUIRED B property is a string that identifies the parameters governing the exchange of messages between the parties. The recipient of a message MUST be able to resolve the B to an individual set of parameters, taking into account the sender of the message. The value of a B property MUST be unique within a namespace that is mutually agreed by the two parties. This could be a concatenation of the B and B PartyId values, a URI that is prefixed with the Internet domain name of one of the parties, or a namespace offered and managed by some other naming or registry service. It is RECOMMENDED that the B be a URI. The B MAY reference an instance of a CPA as defined in the ebXML Collaboration Protocol Profile and Agreement Specification [ebCPP]. An example of the CPAId element follows: http://example.com/cpas/ourcpawithyou.xml This is set with the Perl fragment: $message->set_CPAId("http://example.com/cpas/ourcpawithyou.xml"); If the parties are operating under a CPA, then the reliable messaging parameters are determined by the appropriate elements from that CPA, as identified by the B property. If a receiver determines that a message is in conflict with the CPA, the appropriate handling of this conflict is undefined by this specification. Therefore, senders SHOULD NOT generate such messages unless they have prior knowledge of the receiver's capability to deal with this conflict. If a receiver chooses to generate an error as a result of a detected inconsistency, then it MUST report it with an B of B and a severity of B. If it chooses to generate an error because the B is not recognized, then it MUST report it with an B of B and a severity of B. =item B The REQUIRED B property is a string identifying the set of related messages that make up a conversation between two Parties. It MUST be unique within the context of the specified B. The I initiating a conversation determines the value of the B property that SHALL be reflected in all messages pertaining to that conversation. The B enables the recipient of a message to identify the instance of an application or process that generated or handled earlier messages within a conversation. It remains constant for all messages within a conversation. The value used for a B is implementation dependent. An example of the B element follows: 20001209-133003-28572 As set by: $message->set_ConversationId("20001209-133003-28572"); Note: Implementations are free to choose how they will identify and store conversational state related to a specific conversation. Implementations SHOULD provide a facility for mapping between their identification scheme and a B generated by another implementation. =item B The REQUIRED B property identifies the service that acts on the message and it is specified by the designer of the service. The designer of the service may be: =over =item * a standards organization, or =item * an individual or enterprise =back Note: In the context of an ebXML business process model, an action equates to the lowest possible role based activity in the Business Process [ebBPSS] (requesting or responding role) and a service is a set of related actions for an authorized role within a party. An example of the B element follows: urn:services:SupplierOrderProcessing Set with: $message->set_service("urn:services:SupplierOrderProcessing"); Note: URIs in the B element that start with the namespace C are reserved for use by this specification. The B element has a single B attribute. If the B attribute is present, it indicates the parties sending and receiving the message know, by some other means, how to interpret the content of the B element. The two parties MAY use the value of the type attribute to assist in the interpretation. If the B attribute is not present, the content of the B element MUST be a URI [RFC2396]. If it is not a URI then report an error with B of B and B of B. =item B The REQUIRED B element identifies a process within a B that processes the Message. B SHALL be unique within the B in which it is defined. The value of the B element is specified by the designer of the service. An example of the B element follows: NewOrder If the value of either the B or B element are unrecognized by the I, then it MUST report the error with an B of B and a B of B. =item B The REQUIRED MessageData element provides a means of uniquely identifying an ebXML Message. It contains the following: · MessageId element · Timestamp element · RefToMessageId element · TimeToLive element The following fragment demonstrates the structure of the MessageData element: 20001209-133003-28572@example.com 2001-02-15T11:12:12 20001209-133003-28571@example.com =item B The REQUIRED element MessageId is a globally unique identifier for each message conforming to MessageId [RFC2822]. Note: In the Message-Id and Content-Id MIME headers, values are always surrounded by angle brackets. However references in mid: or cid: scheme URI's and the MessageId and RefToMessageId elements MUST NOT include these delimiters. =item B The REQUIRED Timestamp is a value representing the time that the message header was created conforming to a dateTime [XMLSchema] and MUST be expressed as UTC. Indicating UTC in the Timestamp element by including the `Z' identifier is optional. =item B The RefToMessageId element has a cardinality of zero or one. When present, it MUST contain the MessageId value of an earlier ebXML Message to which this message relates. If there is no earlier related message, the element MUST NOT be present. For Error messages, the RefToMessageId element is REQUIRED and its value MUST be the MessageId value of the message in error (as defined in section 4.2). =item B If the TimeToLive element is present, it MUST be used to indicate the time, expressed as UTC, by which a message should be delivered to the To Party MSH. It MUST conform to an XML Schema dateTime. In this context, the TimeToLive has expired if the time of the internal clock, adjusted for UTC, of the Receiving MSH is greater than the value of TimeToLive for the message. If the To Party's MSH receives a message where TimeToLive has expired, it SHALL send a message to the From Party MSH, reporting that the TimeToLive of the message has expired. This message SHALL be comprised of an ErrorList containing an error with the errorCode attribute set to TimeToLiveExpired and the severity attribute set to Error. =item B The DuplicateElimination element, if present, identifies a request by the sender for the receiving MSH to check for duplicate messages (see section 6.4.1 for more details). Valid values for DuplicateElimination: * The Ace of spades Gambling is for fools, but thats the way I like it baby, I don't want to live for ever.. and don't forget the joker! =over =item DuplicateElimination present duplicate messages SHOULD be eliminated. =item DuplicateElimination not present this results in a delivery behavior of Best-Effort. The DuplicateElimination element MUST NOT be present if the CPA has duplicateElimination set to never (see section 6.4.1 and section 6.6 for more details). =back =item B The Description element may be present zero or more times. Its purpose is to provide a human readable description of the purpose or intent of the message. The language of the description is defined by a required xml:lang attribute. The xml:lang attribute MUST comply with the rules for identifying anguages specified in XML [XML]. Each occurrence SHOULD have a different value for xml:lang. An example of a Description element follows. Purchase Order for One night in bangkok =item B The ebMS Version. This module supports version 2.0, so that is the default value of this property. The REQUIRED version attribute indicates the version of the ebXML Message Service Header Specification to which the ebXML SOAP Header extensions conform. Its purpose is to provide future versioning capabilities. For conformance to this specification, all of the version attributes on any SOAP extension elements defined in this specification MUST have a value of "2.0". An ebXML message MAY contain SOAP header extension elements that have a value other than "2.0". An implementation conforming to this specification that receives a message with ebXML SOAP extensions qualified with a version other than "2.0" MAY process the message if it recognizes the version identified and is capable of processing it. It MUST respond with an error (details TBD) if it does not recognize the identified version. The version attribute MUST be namespace qualified for the ebXML SOAP Envelope extensions namespace defined above. Use of multiple versions of ebXML SOAP extensions elements within the same ebXML SOAP document, while supported, should only be used in extreme cases where it becomes necessary to semantically change an element, which cannot wait for the next ebXML Message Service Specification version release. =item B The Manifest element MAY be present as a child of the SOAP Body element. The Manifest element is a composite element consisting of one or more Reference elements. Each Reference element identifies payload data associated with the message, whether included as part of the message as payload document(s) contained in a Payload Container, or remote resources accessible via a URL. It is RECOMMENDED that no payload data be present in the SOAP Body. The purpose of the Manifest is: =over =item * to make it easier to directly extract a particular payload associated with this ebXML Message, =item * to allow an application to determine whether it can process the payload without having to parse it. =back The Manifest element is comprised of the following: =over =item =item * an id attribute =item * a version attribute =item * one or more Reference elements =back =item B The Reference element is a composite element consisting of the following subordinate elements: =over =item * zero or more Schema elements ­information about the schema(s) that define the instance document identified in the parent Reference element =item * zero or more Description elements ­a textual description of the payload object referenced by the parent =back The Reference element itself is a simple link [XLINK]. It should be noted that the use of XLINK in this context is chosen solely for the purpose of providing a concise vocabulary for describing an association. Use of an XLINK processor or engine is NOT REQUIRED, but may prove useful in certain implementations. The Reference element has the following attribute content in addition to the element content described above: =over =item * id ­an XML ID for the Reference element, =item * xlink:type ­this attribute defines the element as being an XLINK simple link. It has a fixed value of 'simple', =item * xlink:href ­this REQUIRED attribute has a value that is the URI of the payload object referenced. It SHALL conform to the XLINK [XLINK] specification criteria for a simple link. =item * xlink:role ­this attribute identifies some resource that describes the payload object or its purpose. If present, then it SHALL have a value that is a valid URI in accordance with the [XLINK] specification, =item * Any other namespace-qualified attribute MAY be present. A Receiving MSH MAY choose to ignore any foreign namespace attributes other than those defined above. =back The designer of the business process or information exchange using ebXML Messaging decides what payload data is referenced by the Manifest and the values to be used for xlink:role. =item B If the item being referenced has schema(s) of some kind that describe it (e.g. an XML Schema, DTD and/or a database schema), then the Schema element SHOULD be present as a child of the Reference element. It provides a means of identifying the schema and its version defining the payload object identified by the parent Reference element. The Schema element contains the following attributes: =over =item location the REQUIRED URI of the schema =item version a version identifier of the schema =back =back =head2 B If an xlink:href attribute contains a URI that is a content id (URI scheme "cid") then a MIME part with that content-id MUST be present in the corresponding Payload Container of the message. If it is not, then the error SHALL be reported to the From Party with an errorCode of MimeProblem and a severity of Error. If an xlink:href attribute contains a URI, not a content id (URI scheme "cid"), and the URI cannot be resolved, it is an implementation decision whether to report the error. If the error is to be reported, it SHALL be reported to the From Party with an errorCode of MimeProblem and a severity of Error. Note: If a payload exists, which is not referenced by the Manifest, that payload SHOULD be discarded. =cut use strict 'vars', 'subs'; our $fields = { string => { 'Version' => { init_default => "2.0", }, 'CPAId' => { required => 1 }, 'Action' => undef, 'Namespace' => { init_default => 'http://www.oasis-open.org/committees/ebxml-msg/schema/msg-header-2_0.xsd', }, 'MessageId' => undef, 'RefMessageToId' => undef, 'Timestamp' => { init_default => \&generateTimestamp }, 'ConversationId' => { required => 1 }, }, ref => { 'From' => { class => "ebXML::Message::ToFrom", }, 'To' => { class => "ebXML::Message::ToFrom" }, 'Service' => { class => "ebXML::Message::Service" }, 'Manifest' => { class => "ebXML::Message::Manifest" }, 'DuplicateElimination' => { class => "ebXML::Message::DuplicateElimination" }, }, }; BEGIN { # FIXME - should this go into Class::Tangram ? my %setters = ( From => "Message::Party", To => "Message::Party", Service => "ebXML::Message::Service" ); while ( my ($attrib, $class) = each %setters ) { my $class = "Message::Party"; my $setter = "set_$attrib"; *{$setter} = sub { my $self = shift; my $val = shift; if (ref $val eq "ARRAY") { $val = $class->new(@$val) }; return eval '$self->SUPER::'."$setter".'($val, @_);' } } } =head1 METHODS Each object property has a B<$message-Eget_X> and B<$message-Eset_X> method, which get and set the value, respectively. You can also use the simple B<$message-EX> as a getter. Additionally, the following methods may be called on B objects: =over =item B<$message-EgetMIMEParts> returns a list of MIME::Entity objects built using the addPayload method no arguments =cut sub getMIMEParts { my $self = shift; my @parts = (); if ($self->haveMIMEParts) { @parts = values %{$self->{_sekrit}{MIME}{parts}}; warn "returning mimeparts\n"; } else { warn "we have no MIMEParts!\n"; } return @parts; } =item B<$message-EhaveMIMEParts> returns the count of MIME parts currently in the payload I love you honeybunny.. I love you too pumpkin.. Everybody be cool this is a robbery! =cut sub haveMIMEParts { my $self = shift; my $count = $self->{_sekrit}{MIME}{partcount} || 0; return $count; } =item B<$message-EgetMIMEPart> returns a MIME::Entity object built using the addPayload method based on the name given as the first and only argument =cut sub getMIMEPart { my ($self,$name) = @_; my $part = $self->{_sekrit}{MIME}{parts}{$name}; return $part; } =item B<$message-EaddPayload> Adds a payload to the message - takes either a set of options and a MIME::Entity object or string or filename An Entity ( a MIME::Entity object ) or data (a scalar holding the mime payload content) or a path/filename to an existing and accessable file (that will make up the mime payload) are required. Also required are SchemaLocation, and Role (these relate to the ebXML rather than MIME itself) optional arguments are description, Name, filename (required unless full path provided), path (required unless filename provided and includes full path), version (of Schema), content-id $name = $message->addPayload(name=>'Foo',data=>$data, 'content-id'=>'payload-d',filename=>$filename, Description => 'Purchase Order for 100,000 widgets', SchemaLocation=> 'http://regrep.org/gci/purchaseOrder/po.xsd', Role => 'http://regrep.org/gci/purchaseOrder', Name=>'PurchaseOrder', ); or a MIME::Entity object $name = $message->addpayload ( Entity => $entity, Description => 'Purchase Order for 100,000 widgets', SchemaLocation=> 'http://regrep.org/gci/purchaseOrder/po.xsd', Role => 'http://regrep.org/gci/purchaseOrder', Name=>'PurchaseOrder', ); =cut sub addPayload { my ($self, %options) = @_; my $name; $options{Entity} ||= $options{entity}; $options{Description} ||= 'Not Applicable'; $options{SchemaLocation} ||= 'default.xsd'; $options{SchemaVersion} ||= '1'; if ($options{Entity}) { $options{'content-id'} ||= $options{Entity}->head->mime_attr("content-id") || generate_content_id(%options); $name = $options{Entity}->head->mime_attr("content-id") || generate_content_id(%options) ; } else { my $id = generate_content_id(%options); $name = $options{name} || $options{Name} || generate_content_id(%options); my %arguments = (Disposition => "attachment", Type => "text/xml",); $options{filename} ||= $options{Filename} || '$name.tmp'; $options{path} ||= $options{Path} || "/tmp/$options{filename}"; $options{data} ||= $options{Data}; unless ( -f $options{path} ) { open (TMP,">$options{path}") or die "can't create tmp file for MIME part ( $options{path} ) : $!\n"; print TMP $options{data}; close TMP; push(@{$self->{sekrit}{tmp_files}},$options{path} ); } $options{'content-id'} = $id; my $mime_part = MIME::Entity->build ( Path => $options{path}, Filename => $options{filename}, Id => $options{'content-id'}); $options{Entity} = $mime_part; } $self->{_sekrit}{MIME}{parts}{$name} = $options{Entity}; $self->{_sekrit}{MIME}{partcount}++; $self->Manifest->References_insert ( ebXML::Message::Reference->new( Description => ebXML::Message::Description->new(VALUE => $options{Description}, , xml_lang=> 'en-GB',), Schema => ebXML::Message::Schema->new(Version => $options{SchemaVersion}, Location => $options{SchemaLocation}, ), id => $options{'content-id'}, xlink_href => "cid:$options{'content-id'}", xlink_role => $options{Role}, ), ); return $name; } =item B<$message-EremovePayload> removes a named payload from from the message, returns 1 or 0 depending if present or not =cut sub removePayload { my ($self, $name) = @_; my $success = 0; if ($self->{_sekrit}{MIME}{parts}{name}) { delete $self->{_sekrit}{MIME}{parts}{name}; $success++; } return $success; } =item B<$message-EgetOutput> returns one of : Xerces DOM Document object, SOAP::Data::Builder object, XML (generated via Xerces / DOM), XML (generated by SOAP::Data::Builder) depending on mode accepts one argument : mode which can be any of 'dom-xml','dom-doc', 'soap-xml','soap-data' =cut sub getOutput { my ($self,$mode) = @_; my $output; MODE: { if (lc($mode) eq 'dom-xml') { use ebXML::Message::DOMWriter; $output = ebXML::Message::DOMWriter::getOutput(ebXML::Message::DOMWriter::databuilder($self)); last; } if (lc($mode) eq 'dom-doc') { use ebXML::Message::DOMWriter; $output = ebXML::Message::DOMWriter::databuilder($self); last; } if (lc($mode) eq 'soap-data') { use ebXML::Message::SOAPWriter; $output = ebXML::Message::SOAPWriter::databuilder($self); last; } if (lc($mode) eq 'soap-xml') { use ebXML::Message::SOAPWriter; $output = ebXML::Message::SOAPWriter::getOutput(ebXML::Message::SOAPWriter::databuilder($self)); last; } warn " no such mode : $mode ! \n"; } # end of MODE return $output; } =back =cut sub DESTEROY { warn "DESTEROY called\n"; my $self = shift; if (ref $self->{sekrit}{tmp_files}) { foreach my $file ( @{$self->{sekrit}{tmp_files}} ) { warn "removing temp file $file \n"; unlink $file or warn "ERROR : unable to remove temp file ($file) : $!\n"; } } else { warn "no temporary files to clean up\n"; } } ######################################################################### sub generate_content_id { my ($self,%options) = @_; my $date = time; my $content_id; foreach my $option ( keys %options ) { OPTION: { if (lc $option eq 'content-id') { $content_id = $options{$option}; last; } if (lc $option eq 'name') { $content_id = "$options{$option}-$date"; last; } if (lc $option eq 'filename') { $content_id = "$options{$option}-$date"; last; } if (lc $option eq 'path') { ($content_id) = reverse (split(/[\/\\]/,$options{$option})); $content_id .= "-$date"; last; } } # end of OPTION } return $content_id; } sub generateTimestamp { my $self = shift; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = (localtime); my $timestamp = sprintf("%4d-%02d-%02dT%02s:%02s:%02d",$year+1900, $mon+1, $mday, $hour, $min, $sec); # 2000-07-25T12:19:05 warn "timestamp : $timestamp\n"; if ($self) { $self->Timestamp($timestamp) unless ($self->Timestamp); } return $timestamp; } sub requiresAction { my ($self,$value) = @_; if ($value) { $self->{_sekrit}{requiresAction} = $value; } my $yesno = $self->{_sekrit}{requiresAction} || 0; return $yesno; } ######################################################################### # internal / private methods ######################################################################### sub new_from_DOMDocument { my ($self,$doc) = @_; use ebXML::Message::DOMReader; my $class = ebXML::Message::DOMReader->new($doc); return $class; } ######################################################################### # Subclasses for ebXML::Message ######################################################################### # ebXML::Message::Party package ebXML::Message::Party; use base qw(Class::Tangram); our $fields = { string => [ qw(PartyId Type VALUE) ] }; # # ebXML::Message::Manifest package ebXML::Message::Manifest; use base qw(Class::Tangram); our $fields = { string => [ qw(id Version ) ], set => { References => { class => "ebXML::Message::Reference"} },}; # # ebXML::Message::Description package ebXML::Message::Description; use base qw(Class::Tangram); our $fields = { string => [ qw(VALUE xml_lang) ] }; # # ebXML::Message::Schema package ebXML::Message::Schema; use base qw(Class::Tangram); our $fields = { string => [qw(Version Location)] }; # # ebXML::Message::DuplicateElimination package ebXML::Message::DuplicateElimination; use base qw(Class::Tangram); our $fields = { string => [qw(VALUE foo bar)] }; # # ebXML::Message::ToFrom package ebXML::Message::ToFrom; use base qw(Class::Tangram); our $fields = { set => { Roles => { class => "ebXML::Message::Role" }, Partys => { class => "ebXML::Message::Party" } } }; # # ebXML::Message::Role package ebXML::Message::Role; use base qw(Class::Tangram); our $fields = { string => [ qw(VALUE)] }; ################################################################### =head1 EXAMPLES =over =item MessageHeader The following fragment demonstrates the structure of the MessageHeader element within the SOAP Header: uri:example.com http://rosettanet.org/roles/Buyer QRS543 http://rosettanet.org/roles/Seller http://www.oasis-open.org/cpa/123456 987654321 QuoteToCollect NewPurchaseOrder UUID-2 2000-07-25T12:19:05 UUID-1 =item B The following fragment demonstrates a typical Manifest for a single payload MIME body part: Purchase Order for 100,000 widgets =back =cut ################################################################### ################################################################### 1;