Class: EDI::E::Interchange

Inherits:
Interchange show all
Defined in:
lib/edi4r/edifact.rb,
lib/edi4r/edifact-rexml.rb

Overview

Interchange: Class of the top-level objects of UN/EDIFACT data

Constant Summary collapse

@@interchange_defaults =
{
  :i_edi => false, :charset => 'UNOB', :version => 3,
  :show_una => true, :una_string => nil,
  :sender => nil, :recipient => nil,
  :interchange_control_reference => '1', :application_reference => nil,
  :interchange_agreement_id => nil,
  :acknowledgment_request => nil, :test_indicator => nil,
  :output_mode => :verbatim
}
@@interchange_default_keys =
@@interchange_defaults.keys

Instance Attribute Summary collapse

Attributes inherited from Interchange

#illegal_charset_pattern, #output_mode, #syntax, #version

Attributes inherited from Collection_HT

#header, #trailer

Attributes inherited from Object

#name, #parent, #root

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Interchange

detect, #each_BCDS, #fmt_of_DE

Methods inherited from Collection_HT

#empty?, #root=, #to_xml_header, #to_xml_trailer

Methods inherited from Collection

#==, #[], #each, #find_all, #first, #index, #last, #length, #map, #names, #normalized_class_name, #root=, #size

Constructor Details

#initialize(user_par = {}) ⇒ Interchange

Create an empty UN/EDIFACT interchange

Supported parameters (passed hash-style):

Essentials, should not be changed later

:charset

Sets S001.0001, default = 'UNOB'

:version

Sets S001.0002, default = 3

:i_edi

Interactive EDI mode, a boolean (UIB instead of UNB …), default = false

Optional parameters affecting to_s, with corresponding setters

:show_una

Adds UNA sement to output, default = true

:output_mode

See setter output_mode=(), default = :verbatim

:una_string

See class UNA for setters, default = nil

Optional UNB presets for your convenience, may be changed later

:sender

Presets DE S002/0004, default = nil

:recipient

Presets DE S003/0010, default = nil

:interchange_control_reference

Presets DE 0020, default = '1'

:application_reference

Presets DE 0026, default = nil

:interchange_agreement_id

Presets DE 0032, default = nil

:acknowledgment_request

Presets DE 0031, default = nil

:test_indicator

Presets DE 0035, default = nil

Notes

  • Date and time in S004 are set to the current values automatically.

  • Add or change any data element later. except those in S001.

Examples:

  • ic = EDI::E::Interchange.new # Empty interchange, default settings

  • ic = EDI::E::Interchange.new(:charset=>'UNOC',:output_mode=>:linebreak)



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/edi4r/edifact.rb', line 407

def initialize( user_par={} )
  super( user_par ) # just in case...
  if (illegal_keys = user_par.keys - @@interchange_default_keys) != []
    msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
    msg += "Valid param keys (symbols): #{@@interchange_default_keys.join(', ')}"
    raise ArgumentError, msg
  end
  par = @@interchange_defaults.merge( user_par )

  @messages_created = @groups_created = 0

  @syntax = 'E' # par[:syntax]	# E = UN/EDIFACT
  @e_iedi = par[:i_edi]
  @charset = par[:charset]
  @version = par[:version]
  @una = UNA.new(self, par[:una_string])
  self.output_mode = par[:output_mode]
  self.show_una = par[:show_una]

  check_consistencies
  init_ndb( @version )

  if @e_iedi  # Interactive EDI

    raise "I-EDI not supported yet"

    # Fill in what we already know about I-EDI:

    @header = new_segment('UIB')
    @trailer = new_segment('UIZ')
    @header.cS001.d0001 = par[:charset]
    @header.cS001.d0002 = par[:version]

    @header.cS002.d0004 = par[:sender] unless par[:sender].nil?
    @header.cS003.d0010 = par[:recipient] unless par[:recipient].nil?
    @header.cS302.d0300 = par[:interchange_control_reference]
    # FIXME: More to do in S302...

    x= :test_indicator;           @header.d0035 = par[x] unless par[x].nil?

    t = Time.now
    @header.cS300.d0338 = t.strftime(par[:version]==4 ? '%Y%m%d':'%y%m%d')
    @header.cS300.d0314 = t.strftime("%H%M")

    @trailer.d0036 = 0
    ch, ct = @header.cS302, @trailer.cS302
    ct.d0300, ct.d0303, ct.d0051, ct.d0304 = ch.d0300, ch.d0303, ch.d0051, ch.d0304
  else # Batch EDI

    @header = new_segment('UNB')
    @trailer = new_segment('UNZ')
    @header.cS001.d0001 = par[:charset]
    @header.cS001.d0002 = par[:version]
    @header.cS002.d0004 = par[:sender] unless par[:sender].nil?
    @header.cS003.d0010 = par[:recipient] unless par[:recipient].nil?
    @header.d0020 = par[:interchange_control_reference]

    x= :application_reference;    @header.d0026 = par[x] unless par[x].nil?
    x= :acknowledgment_request;   @header.d0031 = par[x] unless par[x].nil?
    x= :interchange_agreement_id; @header.d0032 = par[x] unless par[x].nil?
    x= :test_indicator;           @header.d0035 = par[x] unless par[x].nil?

    t = Time.now
    @header.cS004.d0017 = t.strftime(par[:version]==4 ? '%Y%m%d':'%y%m%d')
    @header.cS004.d0019 = t.strftime("%H%M")

    @trailer.d0036 = 0
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class EDI::Collection

Instance Attribute Details

#charsetObject (readonly)

Returns the value of attribute charset



361
362
363
# File 'lib/edi4r/edifact.rb', line 361

def charset
  @charset
end

#e_indentObject (readonly)

:nodoc:



360
361
362
# File 'lib/edi4r/edifact.rb', line 360

def e_indent
  @e_indent
end

#e_linebreakObject (readonly)

:nodoc:



360
361
362
# File 'lib/edi4r/edifact.rb', line 360

def e_linebreak
  @e_linebreak
end

#groups_createdObject (readonly)

Returns the value of attribute groups_created



362
363
364
# File 'lib/edi4r/edifact.rb', line 362

def groups_created
  @groups_created
end

#messages_createdObject (readonly)

Returns the value of attribute messages_created



362
363
364
# File 'lib/edi4r/edifact.rb', line 362

def messages_created
  @messages_created
end

#show_unaObject

Returns the value of attribute show_una



359
360
361
# File 'lib/edi4r/edifact.rb', line 359

def show_una
  @show_una
end

#unaObject (readonly)

Returns the value of attribute una



361
362
363
# File 'lib/edi4r/edifact.rb', line 361

def una
  @una
end

Class Method Details

.parse(hnd = $stdin, auto_validate = true) ⇒ Object

Reads EDIFACT data from given stream (default: $stdin), parses it and returns an Interchange object



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# File 'lib/edi4r/edifact.rb', line 482

def Interchange.parse( hnd=$stdin, auto_validate=true )
  ic = nil
  buf = hnd.read
  return ic if buf.empty?

  ic, segment_list = Interchange.parse_buffer( buf )
  # Remember to update ndb to SV4-1 now if d0076 of UNB/S001 tells so

  # Deal with 'trash' after UNZ

  if ic.is_iedi?
    init_seg = Regexp.new('^UIB'); tag_init = 'UIB'
    exit_seg = Regexp.new('^UIZ'); tag_exit = 'UIZ'
  else
    init_seg = Regexp.new('^UNB'); tag_init = 'UNB'
    exit_seg = Regexp.new('^UNZ'); tag_exit = 'UNZ'
  end

  last_seg = nil
  loop do
    last_seg = segment_list.pop
    case last_seg
    when /^[A-Z]{3}/ # Segment tag?
      unless last_seg =~ exit_seg
        raise "Parse error: #{tag_exit} is not last segment! Found: #{last_seg}"
      end
      break
    when /\n/, /\r\n/, ''
      # ignore linebreaks at end of file, do not warn.
    else
      warn "WARNING: Data found after #{tag_exit} segment - ignored!"
      warn "Found: \'#{last_seg}\'"
    end
  end
  trailer = Segment.parse(ic, last_seg, tag_exit)

  # Assure that there is only one UNB/UNZ or UIB/UIZ

  err_flag = false
  segment_list.each do |seg|
    if seg =~ init_seg
      warn "ERROR: Another interchange header found in file!"
      err_flag = true
    end
    if seg =~ exit_seg
      warn "ERROR: Another interchange trailer found in file!"
      err_flag = true
    end
  end
  raise "FATAL ERROR - exiting" if err_flag

  # OK, ready to deal with content now:

  case segment_list[0]
  when /^UNH/
    init_seg = Regexp.new('^UNH')
    exit_seg = Regexp.new('^UNT')
    group_mode = false
  when /^UNG/
    init_seg = Regexp.new('^UNG')
    exit_seg = Regexp.new('^UNE')
    group_mode = true
  when /^UIH/ # There is no 'UIG'!
    init_seg = Regexp.new('^UIH')
    exit_seg = Regexp.new('^UIT')
    group_mode = false
  else
    raise "Expected: UNH, UNG, or UIH. Found: #{segment_list[0]}"
  end

  while segbuf = segment_list.shift
    case segbuf

    when init_seg
      sub_list = Array.new
      sub_list.push segbuf

    when exit_seg
      sub_list.push segbuf
      if group_mode
        ic.add( MsgGroup.parse(ic, sub_list), auto_validate )
      else
        ic.add( Message.parse(ic, sub_list), auto_validate )
      end

    else
      sub_list.push segbuf
    end

  end # while

  # Finally add the trailer from the originally read data,
  # thereby overwriting the temporary interchange trailer.
  # Note that the temporary trailer got modified by add()ing
  # to the interchange.
  ic.trailer = trailer
  ic
end

.parse_buffer(buf, s_max = 0) ⇒ Object

INTERNAL USE ONLY: Turn buffer into array of segments (array size <= s_max), read UNB/UIB, create an Interchange object with a header, return this interchange and the array of segments



609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/edi4r/edifact.rb', line 609

def Interchange.parse_buffer( buf, s_max=0 ) # :nodoc:
  case buf
    # UN/EDIFACT case
  when /^(UNA......)?\r?\n?U([IN])B.(UNO[A-Z]).([1-4])/
    par = @@interchange_defaults.dup
    par[:una_string], par[:charset], par[:version], par[:i_edi] =
      $1, $3, $4.to_i, $2=='I'
    ic = Interchange.new( par )
    buf.sub!(/^UNA....../,'') # remove pseudo segment

  else
    raise "Is this really UN/EDIFACT? File starts with: #{buf[0,23]}"
  end

  segments = EDI::E.edi_split(buf, ic.una.seg_term, ic.una.esc_char, s_max)
  # Remove <cr><lf> (some sources are not EDIFACT compliant)
  segments.each {|s| s.sub!(/\s*(.*)/, '\1')}
  ic.header = Segment.parse(ic, segments.shift, ic.is_iedi? ? 'UIB':'UNB')

  [ic, segments]
end

.parse_xml(xdoc) ⇒ Object

Returns a REXML document that represents the interchange

xdoc

REXML document that contains the XML representation of a UN/EDIFACT interchange



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
# File 'lib/edi4r/edifact-rexml.rb', line 34

def Interchange.parse_xml( xdoc )
  _root = xdoc.root
  _header  = _root.elements["Header"]
  _trailer = _root.elements["Trailer"]
  _una  = _header.elements["Parameter[@name='UNA']"]
  _una = _una.text if _una
  raise "Empty UNA" if _una and _una.empty? # remove later!
  # S001: Works for both batch and interactive EDI:
  _s001 =  _header.elements["Segment/CDE[@name='S001']"]
  _version = _s001.elements["DE[@name='0002']"].text.to_i
  _charset = _s001.elements["DE[@name='0001']"].text
  params = { :charset => _charset, :version => _version }
  if _una
    params[:una_string] = _una
    params[:show_una] = true
  end
  ic = Interchange.new( params )
  if _root.elements["Message"].nil? # correct ??
    _root.elements.each('MsgGroup') do |xel|
      ic.add( MsgGroup.parse_xml( ic, xel ), false )
    end
  else
    _root.elements.each('Message') do |xel|
      ic.add( Message.parse_xml( ic, xel ), false )
    end
  end

  ic.header  = Segment.parse_xml( ic, _header.elements["Segment"] )
  ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
  ic.validate
  ic
end

.peek(hnd = $stdin, maxlen = 128) ⇒ Object

Read maxlen bytes from $stdin (default) or from given stream (UN/EDIFACT data expected), and peek into first segment (UNB/UIB).

Returns an empty Interchange object with a properly header filled.

Intended use:

Efficient routing by reading just UNB data: sender/recipient/ref/test


590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/edi4r/edifact.rb', line 590

def Interchange.peek(hnd=$stdin, maxlen=128) # Handle to input stream
  buf = hnd.read( maxlen )
  return nil if buf.empty?
  ic, dummy = Interchange.parse_buffer( buf, 1 )

  # Create a dummy trailer
  tag = ic.is_iedi? ? 'UIZ' : 'UNZ'
  trailer_string = tag.dup << ic.una.de_sep << '0' << ic.una.de_sep << '0'
  ic.trailer= Segment.parse(ic, trailer_string, tag)

  ic
end

.peek_xml(xdoc) ⇒ Object

Read maxlen bytes from $stdin (default) or from given stream (UN/EDIFACT data expected), and peek into first segment (UNB/UIB).

Returns an empty Interchange object with a properly header filled.

Intended use:

Efficient routing by reading just UNB data: sender/recipient/ref/test


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/edi4r/edifact-rexml.rb', line 76

def Interchange.peek_xml(xdoc) # Handle to REXML document
  _root = xdoc.root
  _header  = _root.elements["Header"]
  _trailer = _root.elements["Trailer"]
  _una  = _header.elements["Parameter[@name='UNA']"]
  _una = _una.text if _una
  raise "Empty UNA" if _una and _una.empty? # remove later!
  # S001: Works for both batch and interactive EDI:
  _s001 =  _header.elements["Segment/CDE[@name='S001']"]
  _version = _s001.elements["DE[@name='0002']"].text.to_i
  _charset = _s001.elements["DE[@name='0001']"].text
  params = { :charset => _charset, :version => _version }
  if _una
    params[:una_string] = _una
    params[:show_una] = true
  end
  ic = Interchange.new( params )

  ic.header  = Segment.parse_xml( ic, _header.elements["Segment"] )
  ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )

  ic
end

Instance Method Details

#add(obj, auto_validate = true) ⇒ Object

Add either a MsgGroup or Message object to the interchange. Note: Don't mix both types!

UNZ/UIZ counter DE 0036 is automatically incremented.



672
673
674
675
676
# File 'lib/edi4r/edifact.rb', line 672

def add( obj, auto_validate=true )
  super
  @trailer.d0036 += 1 #if @trailer # @trailer doesn't exist yet when parsing
  # FIXME: Warn/fail if UNH/UIH/UNG id is not unique (at validation?)
end

#inspect(indent = '', symlist = []) ⇒ Object

Yields a readable, properly indented list of all contained objects, including the empty ones. This may be a very long string!



741
742
743
744
# File 'lib/edi4r/edifact.rb', line 741

def inspect( indent='', symlist=[] )
  symlist << :una
  super
end

#is_iedi?Boolean

Returns true if this is an I-EDI interchange (Interactive EDI)

Returns:

  • (Boolean)


634
635
636
# File 'lib/edi4r/edifact.rb', line 634

def is_iedi?
  @e_iedi
end

#new_message(params = {}) ⇒ Object

Derive an empty message from this interchange context. Parameters may be passed hash-like. See Message.new for details



690
691
692
693
# File 'lib/edi4r/edifact.rb', line 690

def new_message(params={})
  @messages_created += 1
  Message.new(self, params)
end

#new_msggroup(params = {}) ⇒ Object

Derive an empty message group from this interchange context. Parameters may be passed hash-like. See MsgGroup.new for details



682
683
684
685
# File 'lib/edi4r/edifact.rb', line 682

def new_msggroup(params={}) # to be completed ...
  @groups_created += 1
  MsgGroup.new(self, params)
end

#new_segment(tag) ⇒ Object

Derive an empty segment from this interchange context For internal use only (header / trailer segment generation)



698
699
700
# File 'lib/edi4r/edifact.rb', line 698

def new_segment(tag) # :nodoc:
  Segment.new(self, tag)
end

#output_mode=(value) ⇒ Object

This method modifies the behaviour of method to_s(): UN/EDIFACT interchanges and their components are turned into strings either “verbatim” (default) or in some more readable way. This method corresponds to a parameter with same name at creation time.

Valid values:

:linebreak

One-segment-per-line representation

:indented

Like :linebreak but with additional indentation (2 blanks per hierarchy level).

:verbatim

No linebreak (default), ISO compliant



650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
# File 'lib/edi4r/edifact.rb', line 650

def output_mode=( value )
  super( value )
  @e_linebreak = @e_indent = ''
  case value
  when :verbatim
    # NOP (default)
  when :linebreak
    @e_linebreak = "\n"
  when :indented
    @e_linebreak = "\n"
    @e_indent = '  '
  else
    raise "Unknown output mode '#{value}'. Supported modes: :linebreak, :indented, :verbatim (default)"
  end
end

#parse_message(list) ⇒ Object

Parse a message (when message mode detected) Internal use only.



713
714
715
# File 'lib/edi4r/edifact.rb', line 713

def parse_message(list) # :nodoc:
  Message.parse(self, list)
end

#parse_msggroup(list) ⇒ Object

Parse a message group (when group mode detected) Internal use only.



706
707
708
# File 'lib/edi4r/edifact.rb', line 706

def parse_msggroup(list) # :nodoc:
  MsgGroup.parse(self, list)
end

#parse_segment(buf, tag) ⇒ Object

Parse a segment (header or trailer expected) Internal use only.



720
721
722
# File 'lib/edi4r/edifact.rb', line 720

def parse_segment(buf, tag) # :nodoc:
  Segment.parse(self, buf, tag)
end

#to_din16557_4(xdoc = REXML::Document.new) ⇒ Object

Returns a REXML document that represents the interchange according to DIN 16557-4



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/edi4r/edifact-rexml.rb', line 122

def to_din16557_4( xdoc = REXML::Document.new )
  externalID = "SYSTEM \"edifact.dtd\""
  doc_element_name = 'EDIFACTINTERCHANGE'
  xdoc << REXML::XMLDecl.new
  xdoc << REXML::DocType.new( doc_element_name, externalID )

  doc_el = REXML::Element.new( doc_element_name )
  xel  = REXML::Element.new( 'UNA' ) 
  xel.attributes["UNA1"]  = una.ce_sep.chr
  xel.attributes["UNA2"]  = una.de_sep.chr
  xel.attributes["UNA3"]  = una.decimal_sign.chr
  xel.attributes["UNA4"]  = una.esc_char.chr
  xel.attributes["UNA5"]  = una.rep_sep.chr
  xel.attributes["UNA6"]  = una.seg_term.chr
  xdoc.elements << doc_el
  doc_el.elements << xel

  super( xdoc.root )
  xdoc
end

#to_sObject

Returns the string representation of the interchange.

Type conversion and escaping are provided. The UNA object is shown when show_una is set to true . See output_mode for modifiers.



731
732
733
734
735
# File 'lib/edi4r/edifact.rb', line 731

def to_s
  s = show_una ? una.to_s + @e_linebreak : ''
  postfix = '' << una.seg_term << @e_linebreak
  s << super( postfix )
end

#to_xml(xdoc = REXML::Document.new) ⇒ Object

Returns a REXML document that represents the interchange



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/edi4r/edifact-rexml.rb', line 104

def to_xml( xdoc = REXML::Document.new )
  rc = super
  # Add parameter(s) to header in rc[1]
  unless @una.nil? #@una.empty?
    xel = REXML::Element.new('Parameter')
    rc[1] << xel
    xel.attributes["name"] = 'UNA'
    xel.text = @una.to_s
  end
#      rc
  xdoc
end

#validate(err_count = 0) ⇒ Object

Returns the number of warnings found, writes warnings to STDERR



749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/edi4r/edifact.rb', line 749

def validate( err_count=0 )
  if (h=self.size) != (t=@trailer.d0036)
    warn "Counter UNZ/UIZ, DE0036 does not match content: #{t} vs. #{h}"
    err_count += 1
  end
  if (h=@header.cS001.d0001) != @charset
    warn "Charset UNZ/UIZ, S001/0001 mismatch: #{h} vs. #@charset"
    err_count += 1
  end
  if (h=@header.cS001.d0002) != @version
    warn "Syntax version UNZ/UIZ, S001/0002 mismatch: #{h} vs. #@version"
    err_count += 1
  end
  check_consistencies

  if is_iedi?
    if (t=@trailer.cS302.d0300) != (h=@header.cS302.d0300)
      warn "UIB/UIZ mismatch in initiator ref (S302/0300): #{h} vs. #{t}"
      err_count += 1
    end
    # FIXME: Add more I-EDI checks
  else
    if (t=@trailer.d0020) != (h=@header.d0020)
      warn "UNB/UNZ mismatch in refno (DE0020): #{h} vs. #{t}"
      err_count += 1
    end
  end

  # FIXME: Check if messages/groups are uniquely numbered

  super
end