Class: Ea::Transformers::QeaToXmi::Transformer

Inherits:
Object
  • Object
show all
Defined in:
lib/ea/transformers/qea_to_xmi/transformer.rb

Overview

Orchestrates serialization of an Ea::Qea::Database to Sparx XMI.

Walks the package tree starting at root packages, constructing xmi gem models (Xmi::Uml::UmlModel, Xmi::Uml::PackagedElement, Xmi::Uml::OwnedAttribute, Xmi::Uml::OwnedEnd, etc.) from each QEA row, then asks the xmi gem to render them via to_xml(use_prefix: true) to produce Sparx XMI in the canonical mixed-prefix style. The serialized output is run through XmlSanitizer to strip truly-empty elements that the xmi gem's round-trip-oriented VALUE_MAP emits but Sparx XMI does not.

Element-kind dispatch (Class vs Enumeration vs DataType vs Instance) is registry-driven — see CLASSIFIER_BUILDERS. Adding a new kind = adding one entry to that hash, no method change. Polymorphism for XMI element shape lives in the xmi gem's models (xmi:type discriminator on PackagedElement), not here.

This is the FULL-FIDELITY path — no Lutaml::Uml::Document intermediate. Sparx-specific concepts (multiplicities, tagged values, stereotypes, primitive types, instance specifications, association ends) come straight from the QEA tables.

Phase 2 will extend the xmi gem with visibility / isAbstract / classifier / aggregation attributes that the QEA database contains but the xmi gem's models don't yet declare.

Constant Summary collapse

MODEL_NAME =
"EA_Model"
EXPORTER =
"Enterprise Architect"
EXPORTER_VERSION =
"6.5"
RELATIONSHIP_AT_PACKAGE_LEVEL =
{
  "Association" => :association,
  "Aggregation" => :association,
  "Composition" => :association,
  "Dependency"  => :dependency,
  "Usage"       => :dependency,
}.freeze
CLASSIFIER_BUILDERS =

OCP registry: maps EaObject#transformer_type to the builder that constructs the corresponding Xmi::Uml element. To add a new element kind (Signal, Interface, ...), append one entry here — build_classifier requires no edit.

Builders are lambdas evaluated via instance_exec, so they run inside the Transformer instance and can call its private helpers without send/public_send dispatch.

{
  class:       ->(obj) { build_class(obj) },
  enumeration: ->(obj) { build_enumeration(obj) },
  data_type:   ->(obj) { build_data_type(obj) },
  instance:    ->(obj) { build_instance(obj) },
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(database) ⇒ Transformer

Returns a new instance of Transformer.



61
62
63
64
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 61

def initialize(database)
  @database = database
  @context  = Context.new(database: database)
end

Instance Method Details

#build_connectors_extensionObject



122
123
124
125
126
127
128
129
130
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 122

def build_connectors_extension
  connectors = @database.collections[:connectors] || []
  return "" if connectors.empty?

  inner = connectors.map { |conn| connector_xml(conn) }.compact.join("\n")
  return "" if inner.empty?

  "\t\t<connectors>\n#{inner}\n\t\t</connectors>"
end

#build_diagrams_extensionObject



132
133
134
135
136
137
138
139
140
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 132

def build_diagrams_extension
  diagrams = @database.collections[:diagrams] || []
  return "" if diagrams.empty?

  inner = diagrams.map { |dgm| diagram_xml(dgm) }.compact.join("\n")
  return "" if inner.empty?

  "\t\t<diagrams>\n#{inner}\n\t\t</diagrams>"
end

#connector_xml(conn) ⇒ Object

Per-connector XML with xmi:idref to the model-level association (when present) plus source/target references.



144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 144

def connector_xml(conn)
  return nil unless conn.ea_guid

  connector_id = "EAID_#{guid_to_xmi_id(conn.ea_guid)}"
  source_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.start_object_id }
  target_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.end_object_id }
  return nil unless source_obj && target_obj

  name_attr = conn.name ? %( name="#{escape(conn.name)}") : ""
  inner = source_end_xml(conn, source_obj) + target_end_xml(conn, target_obj)
  %(<connector xmi:idref="#{connector_id}"#{name_attr}>\n#{inner}\t\t</connector>)
end

#diagram_xml(dgm) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 177

def diagram_xml(dgm)
  return nil unless dgm.ea_guid

  diagram_id = "EAID_#{guid_to_xmi_id(dgm.ea_guid)}"
  pkg_guid = package_guid_for(dgm)
  pkg_ref = pkg_guid ? "EAPK_#{guid_to_xmi_id(pkg_guid)}" : ""
  lines = [
    "<diagram xmi:id=\"#{diagram_id}\">",
    "\t\t\t<model package=\"#{pkg_ref}\" localID=\"#{dgm.diagram_id}\" owner=\"#{pkg_ref}\"/>",
    "\t\t\t<properties name=\"#{escape(dgm.name.to_s)}\" type=\"#{escape(dgm.diagram_type.to_s)}\"/>",
    "\t\t</diagram>"
  ]
  lines.join("\n")
end

#end_xml(obj, kind) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 165

def end_xml(obj, kind)
  xmi_id = "EAID_#{guid_to_xmi_id(obj.ea_guid)}"
  lines = [
    "\t\t\t<#{kind} xmi:idref=\"#{xmi_id}\">",
    "\t\t\t\t<model ea_localid=\"#{obj.ea_object_id}\" " \
      "type=\"#{obj.object_type}\" name=\"#{escape(obj.name.to_s)}\"/>",
    "\t\t\t\t<role visibility=\"Public\" targetScope=\"instance\"/>",
    "\t\t\t</#{kind}>"
  ]
  lines.join("\n") + "\n"
end

#escape(text) ⇒ Object



206
207
208
209
210
211
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 206

def escape(text)
  text.to_s.gsub("&", "&amp;")
       .gsub("<", "&lt;")
       .gsub(">", "&gt;")
       .gsub('"', "&quot;")
end

#extension_inner_xmlString

Returns XML for the inner content of xmi:Extension: ..., ..., .... Empty when none apply.

Returns:

  • (String)

    XML for the inner content of xmi:Extension: ..., ..., .... Empty when none apply.



113
114
115
116
117
118
119
120
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 113

def extension_inner_xml
  parts = []
  connectors_xml = build_connectors_extension
  parts << connectors_xml unless connectors_xml.empty?
  diagrams_xml = build_diagrams_extension
  parts << diagrams_xml unless diagrams_xml.empty?
  parts.join("\n")
end

#guid_to_xmi_id(guid) ⇒ Object



197
198
199
200
201
202
203
204
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 197

def guid_to_xmi_id(guid)
  return "" unless guid

  guid.to_s.gsub(/[{}]/, "")
          .tr("-", "_")
          .prepend("EA") # truncated below
          .sub(/\AEA/, "")
end

#inject_extension_content(xml) ⇒ Object

Injects EA-specific extension content (connectors, diagrams) into the serialized XMI. The xmi gem's Extension type is modeled but doesn't declare connectors / diagrams children. We post-process the serialized XML to insert them inside the existing <xmi:Extension> element.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 94

def inject_extension_content(xml)
  content = extension_inner_xml
  return xml if content.empty?

  # Replace the empty `<xmi:Extension .../>` or the
  # closing `</xmi:Extension>` with the populated version.
  if xml.include?("<xmi:Extension")
    xml.sub(%r{<xmi:Extension ([^>]+)>\s*</xmi:Extension>},
           "<xmi:Extension \\1>\n#{content}\n\t</xmi:Extension>")
       .sub(/<xmi:Extension ([^>]+)\/>/,
            "<xmi:Extension \\1>\n#{content}\n\t</xmi:Extension>")
  else
    xml
  end
end

#package_guid_for(dgm) ⇒ Object



192
193
194
195
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 192

def package_guid_for(dgm)
  pkg = (@database.collections[:packages] || []).find { |p| p.package_id == dgm.package_id }
  pkg&.ea_guid || dgm.ea_guid
end

#serialize(with_extensions: true) ⇒ String

The xmi gem's VALUE_MAP is generation-friendly (to: { nil: :omitted, ... }), so empty collections and nil-valued attributes are skipped at the source. No post-processing pass is needed — the prior XmlSanitizer workaround (TODO 21 §1) has been removed. Serialize to XMI XML.

Default produces model tree only (parseable by the xmi gem's Sparx parser for round-trip). Pass with_extensions: true to also emit EA-specific <connectors> and <diagrams> sections inside <xmi:Extension>. Those use post-processing string injection that the xmi gem's parser doesn't model, so the output is NOT round-trippable when extensions are included.

Parameters:

  • with_extensions (Boolean) (defaults to: true)

Returns:

  • (String)

    XMI XML document

  • (String)

    XMI XML



84
85
86
87
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 84

def serialize(with_extensions: true)
  xml = build_root.to_xml(use_prefix: true)
  with_extensions ? inject_extension_content(xml) : xml
end

#source_end_xml(conn, source_obj) ⇒ Object



157
158
159
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 157

def source_end_xml(conn, source_obj)
  end_xml(source_obj, "source")
end

#target_end_xml(conn, target_obj) ⇒ Object



161
162
163
# File 'lib/ea/transformers/qea_to_xmi/transformer.rb', line 161

def target_end_xml(conn, target_obj)
  end_xml(target_obj, "target")
end