Class: AceMQ::AMQP::XMLCodec

Inherits:
Object
  • Object
show all
Defined in:
lib/acemq/amqp/codec/xml.rb

Overview

Reads and writes XML.

Here because most estates have something that speaks XML and will not be rewritten, and a messaging library that cannot talk to it forces a translation layer nobody wants to own. New services should publish JSON; this exists so the ones that cannot are not a special case.

No document type declaration, ever

A body carrying a <!DOCTYPE> is refused before it is parsed. A DTD is how an XML message reads files off the machine handling it, opens connections on its behalf, and expands twelve bytes into a gigabyte of heap — and a queue is exactly the sort of place a message from somewhere unexpected arrives. This is not configurable, because the configuration would only ever be wrong, and it costs nothing: the Java library disables DTD support outright and neither Jackson nor Go's encoding/xml writes one.

What a decoded message looks like

XML has no types, so every leaf comes back as a String and the caller converts. The root element's name is dropped, because it names the message rather than being part of it — which is also how Jackson and encoding/xml read one into a class:

<order><id>A-1</id><line>widget</line><line>gasket</line></order>
# => { "id" => "A-1", "line" => ["widget", "gasket"] }

Elements repeated under one parent become an Array, which is what makes the Go library's lists readable here; Jackson wraps its lists in an element of their own instead, and that comes back as the nesting it is. Attributes become ordinary keys beside the elements, and an element holding both attributes and text puts the text under the empty key, which is the convention Jackson uses reading into a Map.

REXML

Ruby's own XML parser, and required lazily. It ships with Ruby, but it has been a bundled gem rather than a default one since Ruby 3.4, which means a Bundler process only has it if a Gemfile says so. The gem declares no runtime dependencies, so the failure is caught here and re-raised saying exactly that rather than leaving somebody with +cannot load such file -- rexml/document+.

Constant Summary collapse

CONTENT_TYPE =

What this codec writes, and what Java and Go write.

"application/xml"
ALSO_READS =

Predates the registration and is still what a lot of tooling writes.

["text/xml"].freeze
TEXT_KEY =

Where the text goes when an element has attributes as well.

""
ELEMENT_NAME =

A name XML will accept for an element.

/\A[A-Za-z_][\w.-]*\z/

Instance Method Summary collapse

Constructor Details

#initialize(root: "message") ⇒ XMLCodec

Returns a new instance of XMLCodec.

Parameters:

  • root (String) (defaults to: "message") —

    the name of the root element this codec writes. XML has no anonymous document element, and a Hash does not carry a name the way a Java class does, so one has to be chosen. It matters to a Go consumer whose struct declares an XMLName, and to nobody else: Jackson ignores the root name when reading into a class, and so does encoding/xml for a struct without one.



83
84
85
86
87
88
89
90
91
# File 'lib/acemq/amqp/codec/xml.rb', line 83

def initialize(root: "message")
  unless root.to_s.match?(ELEMENT_NAME)
    raise ArgumentError, "#{root.inspect} is not a name XML will take for an element"
  end

  @root = root.to_s
  load_parser!
  freeze
end

Instance Method Details

#can_decode?(content_type) ⇒ Boolean

Accepts application/xml, text/xml and any +...+xml+ media type.

Never a message whose sender set no content type. XML is rarely what arrives unannounced, and a codec that guessed wrong here would turn a readable message into a rejected one.

Returns:

  • (Boolean)


131
132
133
134
135
136
# File 'lib/acemq/amqp/codec/xml.rb', line 131

def can_decode?(content_type)
  lower = content_type.to_s.downcase
  return false if lower.empty?

  lower.start_with?(CONTENT_TYPE, *ALSO_READS) || lower.include?("+xml")
end

#content_type ⇒ Object



93
# File 'lib/acemq/amqp/codec/xml.rb', line 93

def content_type = CONTENT_TYPE

#decode(body) ⇒ Hash, String

Parameters:

  • body (String)

Returns:

  • (Hash, String)

Raises:

  • (DecodeError) —

    when the body is not XML, or carries a DTD



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/acemq/amqp/codec/xml.rb', line 109

def decode(body)
  text = body.to_s
  if text.match?(/<!DOCTYPE/i)
    raise DecodeError,
          "this message carries a document type declaration, which this codec refuses " \
          "to parse: a DTD is how an XML body reads files off the machine handling it " \
          "and expands a few bytes into a heap full of them."
  end

  root = REXML::Document.new(text).root
  raise DecodeError, "this message has no XML element in it" unless root

  read(root)
rescue REXML::ParseException => e
  raise DecodeError, "this message is not XML: #{e.message.lines.first.to_s.strip}"
end

#encode(payload) ⇒ String

Parameters:

  • payload (Hash, Array, Object)

Returns:

  • (String)

Raises:

  • (EncodeError) —

    when a key is not a name XML will take



98
99
100
101
102
103
104
# File 'lib/acemq/amqp/codec/xml.rb', line 98

def encode(payload)
  document = REXML::Document.new
  fill(document.add_element(@root), payload)
  out = +""
  REXML::Formatters::Default.new.write(document, out)
  out
end