ea 0.6.133 → 0.6.135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 020f344524c3f0702e581b349d7cb3f93a7069a3179684ca59440fd4d4762491
4
- data.tar.gz: 46f2d46ab28ba95e5c975fedfa9bcd176c19b2acc4f3803635786f070dd315c2
3
+ metadata.gz: c90b3802a319e6dec5d22f9d196bfc328c942c679925634774dc55780874deef
4
+ data.tar.gz: 737a502a84e3a08c01070024b0793204ae4c460c928fbd531297fbd3d02fdf82
5
5
  SHA512:
6
- metadata.gz: 16f49f774901e21cee845099df07ff8adc9989c3679c03fb49b97692356a81353d40c73c1cdde5dddfcd74b49657744695b61dbf557c96aabebe730fa4f0a7c4
7
- data.tar.gz: 77fbf77c4cc929cb5565f2a9f712275b13e7244591b2dea3245d4ad096889531af5782aa4a8ad4888d9a2944e789fa9edd2657eb11cc5c93c5c6e9f17a7df3a0
6
+ metadata.gz: a3df522a4725e068ec20bd515db2b35c058f270819cf5f503c187c59dc0d1db7b4b21e3e9d886db33c5960f2510bbe55b47ddddb0731f8c22a55b97e8d25b789
7
+ data.tar.gz: a2319133a4bcc7e0473ec82128b8b23205134cf5e09804f60b15c9e0a9a76ebd3795d6dea3566a4827fcc52b91c65dc14c521d93fbf5133ea63aa189bf229098
@@ -58,11 +58,8 @@ module Ea
58
58
  # `model_element_ref` lookups from diagram elements resolve.
59
59
  def index_by_id
60
60
  @index_by_id ||= begin
61
- idx = {}
62
- packages.each do |p|
63
- idx[p.id] = p
64
- alias_eaid_for_package(idx, p)
65
- end
61
+ idx = PackageAliasIndex.new
62
+ packages.each { |p| idx[p.id] = p }
66
63
  classifiers.each { |c| idx[c.id] = c }
67
64
  relationships.each { |r| idx[r.id] = r }
68
65
  stereotypes.each { |s| idx[s.id] = s }
@@ -98,12 +95,6 @@ module Ea
98
95
  property_index[id]
99
96
  end
100
97
 
101
- def alias_eaid_for_package(idx, package)
102
- return unless package.id&.start_with?("EAPK_")
103
-
104
- idx["EAID_#{package.id[5..]}"] = package
105
- end
106
-
107
98
  def root_packages
108
99
  packages.select { |p| p.parent_id.nil? }
109
100
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Model
5
+ # Id-keyed lookup in which a package stored under `EAPK_<guid>`
6
+ # also answers to `EAID_<guid>`, without a second entry: `values`
7
+ # lists each package once.
8
+ class PackageAliasIndex < Hash
9
+ def [](id)
10
+ super || package_alias(id)
11
+ end
12
+
13
+ private
14
+
15
+ def package_alias(id)
16
+ return unless id.is_a?(String) && id.start_with?("EAID_")
17
+
18
+ fetch("EAPK_#{id[5..]}", nil)
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/ea/model.rb CHANGED
@@ -19,6 +19,7 @@ module Ea
19
19
  autoload :Base, "ea/model/base"
20
20
  autoload :Metadata, "ea/model/metadata"
21
21
  autoload :Point, "ea/model/point"
22
+ autoload :PackageAliasIndex, "ea/model/package_alias_index"
22
23
  autoload :Bounds, "ea/model/bounds"
23
24
  autoload :Waypoint, "ea/model/waypoint"
24
25
  autoload :Annotation, "ea/model/annotation"
@@ -35,6 +35,7 @@ module Ea
35
35
  instance_specifications: instance_specifications,
36
36
  diagrams: diagrams
37
37
  )
38
+ mark_connector_proxies(doc)
38
39
  apply_tagged_values(doc)
39
40
  apply_receptions(doc)
40
41
  apply_stereotypes(doc)
@@ -44,6 +45,23 @@ module Ea
44
45
 
45
46
  private
46
47
 
48
+ # EA serializes connector anchors as UML class stubs. Keep their
49
+ # identity and geometry while distinguishing their graphical role.
50
+ def mark_connector_proxies(doc)
51
+ proxy_ids = connector_proxy_ids
52
+ return if proxy_ids.empty?
53
+
54
+ doc.diagrams.flat_map(&:elements).each do |element|
55
+ element.style["connector_proxy"] = true if proxy_ids.include?(element.model_element_ref)
56
+ end
57
+ end
58
+
59
+ def connector_proxy_ids
60
+ (root.extension&.elements&.element || []).filter_map do |element|
61
+ IdNormalizer.from_xmi_id(element.idref) if element.type == "uml:ProxyConnector"
62
+ end
63
+ end
64
+
47
65
  def apply_receptions(doc)
48
66
  grouped = ReceptionBuilder.new(xmi_path).grouped_by_owner
49
67
  doc.classifiers.each do |classifier|
@@ -99,10 +117,9 @@ module Ea
99
117
  end
100
118
  end
101
119
 
102
- # Replaces each type reference with its in-model qualified
103
- # name; references that are not classifiers (primitive hrefs,
104
- # EA placeholder idrefs) take the type name EA stores in the
105
- # extension block.
120
+ # Replaces each type reference with the type name EA stores in
121
+ # the extension block; references the extension does not name
122
+ # fall back to the classifier's in-model qualified name.
106
123
  def resolve_type_names(doc)
107
124
  qualified_by_id = doc.classifiers.each_with_object({}) do |c, acc|
108
125
  acc[c.id] = c.qualified_name if c.id && c.qualified_name
@@ -111,8 +128,8 @@ module Ea
111
128
 
112
129
  doc.classifiers.each do |classifier|
113
130
  (classifier.properties || []).each do |prop|
114
- prop.type_name = qualified_by_id[prop.type_ref] ||
115
- types.attribute_type(prop.id) || prop.type_name
131
+ prop.type_name = types.attribute_type(prop.id) ||
132
+ qualified_by_id[prop.type_ref] || prop.type_name
116
133
  end
117
134
  (classifier.operations || []).each do |op|
118
135
  resolve_operation_types(op, qualified_by_id, types)
@@ -121,12 +138,12 @@ module Ea
121
138
  end
122
139
 
123
140
  def resolve_operation_types(operation, qualified_by_id, types)
124
- operation.return_type_name = qualified_by_id[operation.return_type_name] ||
125
- types.return_type(operation.id) ||
141
+ operation.return_type_name = types.return_type(operation.id) ||
142
+ qualified_by_id[operation.return_type_name] ||
126
143
  operation.return_type_name
127
144
  operation.parameters.each do |param|
128
- param.type_name = qualified_by_id[param.type_name] ||
129
- types.parameter_type(param.id) || param.type_name
145
+ param.type_name = types.parameter_type(param.id) ||
146
+ qualified_by_id[param.type_name] || param.type_name
130
147
  end
131
148
  end
132
149
 
@@ -16,41 +16,49 @@ module Ea
16
16
 
17
17
  def build_all
18
18
  classifiers = []
19
- walk(root.model) do |element, parent_pkg|
20
- next unless TypeClassifierMap.classifier_type?(element.type)
19
+ root.model.packaged_element.each do |top|
20
+ walk(top, top.type == "uml:Package" ? top : nil) do |element, parent_pkg, owner_class|
21
+ next unless TypeClassifierMap.classifier_type?(element.type)
21
22
 
22
- classifiers << build_one(element, parent_pkg)
23
+ classifiers << build_one(element, parent_pkg, owner_class)
24
+ end
23
25
  end
24
26
  classifiers
25
27
  end
26
28
 
27
- def build_one(element, containing_package)
29
+ def build_one(element, containing_package, owner_class = nil)
28
30
  klass = TypeClassifierMap.class_for(element.type)
29
- args = common_args(element, containing_package)
31
+ args = common_args(element, containing_package, owner_class)
30
32
  args.merge!(enumeration_args(element)) if klass == Ea::Model::Enumeration
31
33
  klass.new(**args)
32
34
  end
33
35
 
34
36
  private
35
37
 
36
- # Depth-first walk of the packaged_element tree. Yields each
37
- # element with its containing uml:Package (or nil at the
38
- # model root).
39
- def walk(element, parent_pkg = nil, &block)
40
- yield element, parent_pkg if block
38
+ # Depth-first walk of the packaged_element and nestedClassifier
39
+ # trees. Yields each element with its containing uml:Package
40
+ # (nil at the model root) and the uml:Class that nests it
41
+ # (nil unless it is a nestedClassifier of a class).
42
+ def walk(element, parent_pkg = nil, owner_class = nil, &block)
43
+ yield element, parent_pkg, owner_class if block
41
44
  element.packaged_element.each do |inner|
42
45
  new_parent = inner.type == "uml:Package" ? inner : parent_pkg
43
- walk(inner, new_parent, &block)
46
+ walk(inner, new_parent, nil, &block)
47
+ end
48
+ element.nested_classifier.each do |inner|
49
+ walk(inner, parent_pkg, element, &block)
44
50
  end
45
51
  end
46
52
 
47
- def common_args(element, containing_package)
53
+ def common_args(element, containing_package, owner_class)
48
54
  id = IdNormalizer.from_xmi_id(element.id)
55
+ nested_parent = owner_class&.name
49
56
  {
50
57
  id: id,
51
58
  name: element.name,
52
59
  package_id: containing_package && IdNormalizer.from_xmi_id(containing_package.id),
53
- qualified_name: qualified_name(element, containing_package),
60
+ nested_parent_name: nested_parent,
61
+ qualified_name: qualified_name(element, containing_package, nested_parent),
54
62
  is_abstract: boolean(element.is_abstract),
55
63
  visibility: element.visibility,
56
64
  properties: PropertyBuilder.new.build_all_for(element),
@@ -82,18 +90,16 @@ module Ea
82
90
  { literals: literals }
83
91
  end
84
92
 
85
- def qualified_name(element, containing_package)
86
- # The XMI's `name` attribute may already be in
87
- # "ns:Class" form (e.g. "xs:string" lives in the `xs`
88
- # package). Forcing the package name in front would
89
- # produce "xs::xs:string". Strip the existing namespace
90
- # prefix from the simple name before composing the
91
- # qualified name.
92
- raw = element.name.to_s
93
- simple = raw.split(":").last
94
- simple = raw if simple.nil? || simple.empty?
93
+ # A name that already carries a namespace prefix
94
+ # (e.g. "gml:CodeType") is the qualified name as saved; a
95
+ # nested classifier is prefixed with its owner class.
96
+ def qualified_name(element, containing_package, nested_parent)
97
+ name = element.name.to_s
98
+ return name if name.include?(":")
99
+ return "#{nested_parent}::#{name}" if nested_parent
100
+
95
101
  pkg_name = containing_package&.name
96
- pkg_name ? "#{pkg_name}::#{simple}" : simple
102
+ pkg_name ? "#{pkg_name}::#{name}" : name
97
103
  end
98
104
 
99
105
  def boolean(value)
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Ea
6
+ module Sources
7
+ module Xmi
8
+ class DiagramBuilder
9
+ # The XMI gem omits role stereotypes. Stream the saved connector
10
+ # records once, without retaining another full XML document.
11
+ class ConnectorMetadata
12
+ def initialize(xmi_path)
13
+ @xmi_path = xmi_path
14
+ end
15
+
16
+ def for(connector)
17
+ {
18
+ source_stereotype: role_stereotypes.dig(connector.idref, :source),
19
+ target_stereotype: role_stereotypes.dig(connector.idref, :target),
20
+ is_substitutable: false_substitutability?(connector.xrefs&.value.to_s)
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def false_substitutability?(xrefs)
27
+ xrefs.scan(/\$XREFPROP=(.*?)\$ENDXREF;/m).any? do |match|
28
+ record = match.first
29
+ next false unless record.include?("$NAM=CustomProperties$NAM;") &&
30
+ record.include?("$TYP=connector property$TYP;")
31
+
32
+ values = record.scan(/@PROP=(.*?)@ENDPROP;/m).filter_map do |property|
33
+ substitutability_value(property.first)
34
+ end
35
+ !values.empty? && !values.include?("-1")
36
+ end
37
+ end
38
+
39
+ def substitutability_value(property)
40
+ return unless property.include?("@NAME=isSubstitutable@ENDNAME;") &&
41
+ property.include?("@TYPE=Boolean@ENDTYPE;")
42
+
43
+ property[/@VALU=(.*?)@ENDVALU;/m, 1]
44
+ end
45
+
46
+ def role_stereotypes
47
+ @role_stereotypes ||= read_role_stereotypes
48
+ end
49
+
50
+ def read_role_stereotypes
51
+ return {} unless @xmi_path
52
+
53
+ File.open(@xmi_path) do |file|
54
+ Nokogiri::XML::Reader(file).each_with_object({}) do |node, roles|
55
+ next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT && node.local_name == "connector"
56
+
57
+ read_connector_roles(node, roles)
58
+ end
59
+ end
60
+ end
61
+
62
+ def read_connector_roles(node, roles)
63
+ connector = Nokogiri::XML(node.outer_xml).root
64
+ id = connector.at_xpath("@*[local-name()='idref']")&.value
65
+ return unless id
66
+
67
+ roles[id] = %i[source target].to_h do |side|
68
+ value = connector.at_xpath("./*[local-name()='#{side}']/*[local-name()='role']/@stereotype")&.value
69
+ [side, value]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Ea
6
+ module Sources
7
+ module Xmi
8
+ class DiagramBuilder
9
+ # The XMI gem exposes placement fonts but drops object styleex.
10
+ # Read saved defaults once, then let each placement override them.
11
+ class FontSettings
12
+ def initialize(xmi_path)
13
+ @xmi_path = xmi_path
14
+ end
15
+
16
+ def for(placed)
17
+ style = placed.style
18
+ defaults = object_styles.fetch(placed.subject, {})
19
+ placement = ExtensionStyleParser.split_pairs(style.raw.to_s)
20
+ raw_size = saved_size(placement) || saved_size(defaults)
21
+ {
22
+ family: presence(style.font_family) || defaults["font"],
23
+ size: style.font_size || ExtensionStyleParser.scale_font_size(raw_size),
24
+ logical_height: logical_height(raw_size)
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def presence(value)
31
+ value unless value.nil? || value.empty?
32
+ end
33
+
34
+ def saved_size(style)
35
+ value = style["fontsz"]
36
+ value if ExtensionStyleParser.int(value)&.nonzero?
37
+ end
38
+
39
+ def logical_height(value)
40
+ tenths = ExtensionStyleParser.int(value)
41
+ tenths * 96 / 720 if tenths
42
+ end
43
+
44
+ def object_styles
45
+ @object_styles ||= saved_objects.each_with_object({}) do |element, styles|
46
+ id = element.at_xpath("@*[local-name()='idref']")&.value
47
+ style = element.at_xpath("./*[local-name()='style']/@styleex")&.value
48
+ styles[id] = ExtensionStyleParser.split_pairs(style) if id && style
49
+ end
50
+ end
51
+
52
+ def saved_objects
53
+ return [] unless @xmi_path
54
+
55
+ doc = Nokogiri::XML(File.read(@xmi_path))
56
+ doc.xpath("//*[local-name()='Extension']/*[local-name()='elements']/*[local-name()='element']")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Xmi
6
+ class DiagramBuilder
7
+ # Keep graphical spelling provenance separate from semantic stereotype names.
8
+ class StereotypeProfiles
9
+ def initialize(root)
10
+ @elements = root.extension&.elements&.element.to_a.to_h { |element| [element.idref, element] }
11
+ end
12
+
13
+ def for(subject)
14
+ element = @elements[subject]
15
+ name = element&.properties&.stereotype
16
+ return {} if name.to_s.empty?
17
+
18
+ fqname = matching_profile(element.xrefs&.value.to_s, name)
19
+ fqname ? { stereotype_fqname: fqname } : {}
20
+ end
21
+
22
+ private
23
+
24
+ def matching_profile(value, name)
25
+ value.split("@STEREO;").drop(1).each do |block|
26
+ description = "@STEREO;#{block.partition("@ENDSTEREO;").first}@ENDSTEREO;"
27
+ application = Qea::Xref::Parser.parse(description).stereotype
28
+ return application.fqname if application.name == name
29
+ end
30
+ nil
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -6,9 +6,10 @@ module Ea
6
6
  module Sources
7
7
  module Xmi
8
8
  class DiagramBuilder
9
- # Ordered waypoints of the UMLDI edge EA exported for a connector
10
- # on a diagram. A connector drawn by more than one edge in the same
11
- # diagram, or by an edge with fewer than two waypoints, has no route.
9
+ # The UMLDI edge EA exported for a connector on a diagram: its
10
+ # ordered waypoints and its source/target model elements. A connector
11
+ # drawn by more than one edge in the same diagram has neither; an edge
12
+ # with fewer than two waypoints has no route.
12
13
  class UmldiEdges
13
14
  def initialize(xmi_path)
14
15
  @xmi_path = xmi_path
@@ -18,8 +19,34 @@ module Ea
18
19
  routes.dig(diagram_id, connector_id)
19
20
  end
20
21
 
22
+ def ends_for(diagram_id, connector_id)
23
+ ends.dig(diagram_id, connector_id)
24
+ end
25
+
21
26
  private
22
27
 
28
+ def ends
29
+ @ends ||= doc.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
30
+ [diagram["id"] || diagram["xmi:id"], diagram_ends(diagram)]
31
+ end
32
+ end
33
+
34
+ def diagram_ends(diagram)
35
+ unique_edges(diagram).each_with_object({}) do |(connector_id, edge), acc|
36
+ acc[connector_id] = [edge["source"], edge["target"]] if edge["source"] && edge["target"]
37
+ end
38
+ end
39
+
40
+ # Edges keyed by connector, keeping only connectors drawn by exactly
41
+ # one edge of the diagram, whatever that edge carries.
42
+ def unique_edges(diagram)
43
+ diagram.xpath("./ownedElement[@modelElement]")
44
+ .select { |node| node.attributes["type"]&.value.to_s.end_with?("UMLEdge") }
45
+ .group_by { |edge| edge["modelElement"] }
46
+ .select { |_, same| same.one? }
47
+ .transform_values(&:first)
48
+ end
49
+
23
50
  def routes
24
51
  @routes ||= doc.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
25
52
  [diagram["id"] || diagram["xmi:id"], diagram_routes(diagram)]
@@ -27,9 +54,8 @@ module Ea
27
54
  end
28
55
 
29
56
  def diagram_routes(diagram)
30
- edges = diagram.xpath("./ownedElement[@modelElement][waypoint]").group_by { |edge| edge["modelElement"] }
31
- edges.each_with_object({}) do |(connector_id, same), acc|
32
- points = same.one? ? waypoints(same.first) : []
57
+ unique_edges(diagram).each_with_object({}) do |(connector_id, edge), acc|
58
+ points = waypoints(edge)
33
59
  acc[connector_id] = points if points.size >= 2
34
60
  end
35
61
  end
@@ -9,6 +9,9 @@ module Ea
9
9
  # SOID/EOID, EDGE, label boxes) when present. Falls back to
10
10
  # the UML-DI owned_element bounds otherwise.
11
11
  class DiagramBuilder
12
+ autoload :ConnectorMetadata, "ea/sources/xmi/diagram_builder/connector_metadata"
13
+ autoload :FontSettings, "ea/sources/xmi/diagram_builder/font_settings"
14
+ autoload :StereotypeProfiles, "ea/sources/xmi/diagram_builder/stereotype_profiles"
12
15
  autoload :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
13
16
 
14
17
  attr_reader :root, :xmi_path
@@ -52,10 +55,14 @@ module Ea
52
55
  name: props&.name || ext_diagram.id,
53
56
  package_id: ext_diagram.model&.package,
54
57
  diagram_type: props&.type&.split(":")&.last&.downcase,
55
- bounds: canvas_bounds(placed_elements),
58
+ bounds: page_bounds(ext_diagram),
56
59
  style: style_for(ext_diagram),
57
60
  style_ex: style_ex_for(ext_diagram),
58
61
  show_notes: style_ex_for(ext_diagram).to_s.include?("ShowNotes=1"),
62
+ show_package_contents: style_flag?(ext_diagram, "PackageContents"),
63
+ show_foreign: style_flag?(ext_diagram, "HighlightForeign"),
64
+ show_parents: !style_flag?(ext_diagram, "HideParents"),
65
+ hand_draw: style_flag?(ext_diagram, "HandDraw"),
59
66
  elements: build_elements(placed_elements, id, umldi_keywords),
60
67
  connectors: build_connectors(placed_connectors,
61
68
  placed_elements, id),
@@ -89,18 +96,18 @@ module Ea
89
96
 
90
97
  private
91
98
 
92
- def canvas_bounds(placed_elements)
93
- return nil if placed_elements.empty?
99
+ def style_flag?(ext_diagram, key)
100
+ value = style_for(ext_diagram).to_s[/(?:\A|;)#{key}=(\d+)/, 1]
101
+ value.to_i.nonzero? ? true : false
102
+ end
94
103
 
95
- left = placed_elements.map { |e| e.geometry.img_left || e.geometry.left }.compact.min
96
- top = placed_elements.map { |e| e.geometry.img_top || e.geometry.top }.compact.min
97
- right = placed_elements.map { |e| e.geometry.img_right || e.geometry.right }.compact.max
98
- bottom = placed_elements.map { |e| e.geometry.img_bottom || e.geometry.bottom }.compact.max
99
- return nil unless left && top && right && bottom
104
+ def page_bounds(ext_diagram)
105
+ style = style_for(ext_diagram).to_s
106
+ width = style[/(?:\A|;)DocSize\.cx=(\d+)/, 1]
107
+ height = style[/(?:\A|;)DocSize\.cy=(\d+)/, 1]
108
+ return nil unless width && height
100
109
 
101
- Ea::Model::Bounds.new(x: 0, y: 0,
102
- width: right - left,
103
- height: bottom - top)
110
+ Ea::Model::Bounds.new(x: 0, y: 0, width: width.to_i, height: height.to_i)
104
111
  end
105
112
 
106
113
  def build_elements(placed, owner_id, umldi_keywords = {})
@@ -110,17 +117,21 @@ module Ea
110
117
  def build_element(placed, owner_id, umldi_keywords = {})
111
118
  geom = placed.geometry
112
119
  style = placed.style
120
+ bounds = build_bounds(geom.left, geom.top, geom.right, geom.bottom)
121
+ font = (@font_settings ||= FontSettings.new(xmi_path)).for(placed)
113
122
  Ea::Model::DiagramElement.new(
114
123
  id: IdNormalizer.synthetic_id(owner_id, "elem", placed.subject),
115
124
  diagram_id: owner_id,
116
125
  model_element_ref: placed.subject,
117
- bounds: build_bounds(geom.left, geom.top, geom.right, geom.bottom),
118
- image_bounds: build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom),
126
+ bounds: bounds,
127
+ image_bounds: (build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom) unless bounds),
119
128
  background_color: style.background_color,
120
129
  line_color: style.line_color,
121
130
  line_width: style.line_width,
122
- font_family: style.font_family,
123
- font_size: style.font_size,
131
+ font_family: font[:family],
132
+ font_size: font[:size],
133
+ font_logical_height: font[:logical_height],
134
+ font_size_inherited: ExtensionStyleParser.split_pairs(style.raw.to_s)["fontsz"] == "0",
124
135
  font_bold: style.bold,
125
136
  font_italic: style.italic,
126
137
  font_underline: style.underline,
@@ -129,7 +140,7 @@ module Ea
129
140
  z_order: placed.seqno,
130
141
  duid: style.duid,
131
142
  umldi_keyword: umldi_keywords[placed.subject],
132
- style: {}
143
+ style: (@stereotype_profiles ||= StereotypeProfiles.new(root)).for(placed.subject)
133
144
  )
134
145
  end
135
146
 
@@ -146,17 +157,19 @@ module Ea
146
157
  by_duid = placed_elements.each_with_object({}) do |pe, acc|
147
158
  acc[pe.style.duid] = pe if pe.style.duid
148
159
  end
160
+ by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
149
161
 
150
162
  placed_connectors.each_with_index.map do |placed, index|
151
- build_connector(placed, by_duid, owner_id, index)
163
+ build_connector(placed, by_duid, by_subject, owner_id, index)
152
164
  end
153
165
  end
154
166
 
155
- def build_connector(placed, by_duid, owner_id, rank)
167
+ def build_connector(placed, by_duid, by_subject, owner_id, rank)
156
168
  geom = placed.geometry
157
169
  style = placed.style
158
- source = by_duid[style.soid]
159
- target = by_duid[style.eoid]
170
+ edge_source, edge_target = umldi_ends(owner_id, placed)
171
+ source = by_duid[style.soid] || sole_placement(by_subject, edge_source)
172
+ target = by_duid[style.eoid] || sole_placement(by_subject, edge_target)
160
173
  ext_meta = connector_meta(placed.subject)
161
174
  route = connector_route(placed, source, target, owner_id, ext_meta[:ea_type])
162
175
  Ea::Model::DiagramConnector.new(
@@ -172,6 +185,9 @@ module Ea
172
185
  target_edge: geom.edge,
173
186
  connector_type: ext_meta[:ea_type],
174
187
  direction: ext_meta[:direction],
188
+ source_stereotype: ext_meta[:source_stereotype],
189
+ target_stereotype: ext_meta[:target_stereotype],
190
+ is_substitutable: ext_meta.fetch(:is_substitutable, false),
175
191
  waypoints: route[:waypoints],
176
192
  style: route[:style],
177
193
  line_color: style.color,
@@ -189,7 +205,11 @@ module Ea
189
205
  {
190
206
  ea_type: props&.ea_type,
191
207
  direction: props&.direction
192
- }
208
+ }.merge(connector_metadata.for(connector))
209
+ end
210
+
211
+ def connector_metadata
212
+ @connector_metadata ||= ConnectorMetadata.new(xmi_path)
193
213
  end
194
214
 
195
215
  def connector_index
@@ -234,6 +254,23 @@ module Ea
234
254
  route&.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
235
255
  end
236
256
 
257
+ def umldi_ends(diagram_id, placed)
258
+ return unless xmi_path
259
+
260
+ umldi_edges.ends_for(diagram_id, placed.subject)
261
+ end
262
+
263
+ def sole_placement(by_subject, subject)
264
+ placements = by_subject[guid_key(subject)]
265
+ placements.first if placements&.one?
266
+ end
267
+
268
+ # EA spells one GUID with an EAID_ or EAPK_ prefix and mixed case
269
+ # depending on the element kind.
270
+ def guid_key(subject)
271
+ subject&.sub(/\AEA(?:ID|PK)_/, "")&.upcase
272
+ end
273
+
237
274
  def umldi_edges
238
275
  @umldi_edges ||= UmldiEdges.new(xmi_path)
239
276
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Xmi
6
+ # Builds an `Ea::Model::Legend` from the `value` of an element's
7
+ # `<xrefs>` element. The CustomProperties description inside it
8
+ # uses the same `@PROP=...@ENDPROP;` encoding as the QEA t_xref
9
+ # row, so parsing is delegated to the QEA legend builder.
10
+ #
11
+ # Returns nil when the value carries no legend properties.
12
+ class LegendBuilder
13
+ CLIENT = "xmi-element"
14
+
15
+ XrefRow = Struct.new(:name, :description)
16
+
17
+ # Answers the one CustomProperties xref lookup the QEA builder makes.
18
+ class SingleElementXrefs
19
+ def initialize(description)
20
+ @rows = [XrefRow.new(Qea::LegendBuilder::LEGEND_XREF_NAME, description)]
21
+ end
22
+
23
+ def xrefs_for_client(_guid)
24
+ @rows
25
+ end
26
+ end
27
+
28
+ def initialize(xrefs_value)
29
+ @xrefs_value = xrefs_value
30
+ end
31
+
32
+ def build
33
+ return nil if @xrefs_value.to_s.empty?
34
+
35
+ Qea::LegendBuilder.new(SingleElementXrefs.new(@xrefs_value)).build_for(CLIENT)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -10,9 +10,12 @@ module Ea
10
10
  # inside `<xmi:Extension>/<elements>`. Each carries a
11
11
  # `<properties documentation="..."/>` child with the body text.
12
12
  #
13
- # The xmi gem parses Note elements as part of the extension
14
- # block; we walk them and project to Ea::Model::Note.
13
+ # `uml:Text` elements are projected the same way; those whose
14
+ # CustomProperties xref carries legend configuration also
15
+ # get an `Ea::Model::Legend` payload.
15
16
  class NoteBuilder
17
+ NOTE_TYPES = { "uml:Note" => "Note", "uml:Text" => "Text" }.freeze
18
+
16
19
  attr_reader :root
17
20
 
18
21
  def initialize(root)
@@ -29,17 +32,23 @@ module Ea
29
32
  private
30
33
 
31
34
  def build_one(ext_element)
32
- type = ext_element.type
33
- return nil unless type == "uml:Note"
35
+ note_type = NOTE_TYPES[ext_element.type]
36
+ return nil unless note_type
34
37
 
35
- props = ext_element.properties
36
38
  Ea::Model::Note.new(
37
39
  id: ext_element.idref,
38
40
  name: ext_element.name,
39
- body: props&.documentation,
40
- note_type: "Note"
41
+ body: ext_element.properties&.documentation,
42
+ note_type: note_type,
43
+ legend: legend_for(ext_element, note_type)
41
44
  )
42
45
  end
46
+
47
+ def legend_for(ext_element, note_type)
48
+ return nil unless note_type == "Text"
49
+
50
+ LegendBuilder.new(ext_element.xrefs&.value).build
51
+ end
43
52
  end
44
53
  end
45
54
  end
@@ -60,12 +60,16 @@ module Ea
60
60
  end
61
61
 
62
62
  def child_classifier_ids(packaged_element)
63
- packaged_element.packaged_element
64
- .filter_map do |pe|
65
- next unless TypeClassifierMap.classifier_type?(pe.type)
63
+ packaged_element.packaged_element.flat_map do |element|
64
+ classifier_ids(element)
65
+ end
66
+ end
67
+
68
+ def classifier_ids(element)
69
+ return [] unless TypeClassifierMap.classifier_type?(element.type)
66
70
 
67
- IdNormalizer.from_xmi_id(pe.id)
68
- end
71
+ [IdNormalizer.from_xmi_id(element.id),
72
+ *element.nested_classifier.flat_map { |inner| classifier_ids(inner) }].compact
69
73
  end
70
74
  end
71
75
  end
@@ -9,6 +9,8 @@ module Ea
9
9
  class RelationshipBuilder
10
10
  # Connector types that imply an aggregation on the source end
11
11
  # when the end itself records none.
12
+ CONNECTOR_STEREOTYPE = /(?<=@STEREO;Name=)[^;]+/
13
+
12
14
  AGGREGATION_FOR_CONNECTOR_TYPE = {
13
15
  "Aggregation" => "shared",
14
16
  "Composition" => "composite"
@@ -22,13 +24,13 @@ module Ea
22
24
 
23
25
  def build_all
24
26
  relationships = []
25
- walk(root.model) do |element|
26
- next unless RelationshipTypeMap.relationship_type?(element.type)
27
-
28
- rel = build_one(element)
29
- relationships << rel if rel
27
+ root.model.packaged_element.each do |top|
28
+ walk(top) do |element|
29
+ relationships << build_one(element) if RelationshipTypeMap.relationship_type?(element.type)
30
+ relationships.concat(owned_generalizations(element))
31
+ end
30
32
  end
31
- relationships
33
+ relationships + package_import_relationships(relationships)
32
34
  end
33
35
 
34
36
  def build_one(element)
@@ -40,9 +42,44 @@ module Ea
40
42
 
41
43
  private
42
44
 
45
+ def package_import_relationships(built)
46
+ built_ids = built.to_set(&:id)
47
+ connector_index.filter_map do |id, connector|
48
+ next if built_ids.include?(id) || connector.properties&.ea_type != "Package"
49
+
50
+ package_import_relationship(id, connector)
51
+ end
52
+ end
53
+
54
+ def connector_stereotype(connector)
55
+ connector&.xrefs&.value.to_s[CONNECTOR_STEREOTYPE]
56
+ end
57
+
58
+ def package_import_relationship(id, connector)
59
+ Ea::Model::Association.new(
60
+ id: id,
61
+ stereotype: connector_stereotype(connector),
62
+ source_id: connector.source&.idref,
63
+ target_id: connector.target&.idref,
64
+ source_navigable: navigable?(id, :source),
65
+ target_navigable: navigable?(id, :target)
66
+ )
67
+ end
68
+
43
69
  def walk(element, &block)
44
70
  yield element if block
45
71
  element.packaged_element.each { |inner| walk(inner, &block) }
72
+ element.nested_classifier.each { |inner| walk(inner, &block) }
73
+ end
74
+
75
+ def owned_generalizations(element)
76
+ element.generalization.map do |generalization|
77
+ Ea::Model::Generalization.new(
78
+ id: IdNormalizer.from_xmi_id(generalization.id),
79
+ specific_id: IdNormalizer.from_xmi_id(element.id),
80
+ general_id: IdNormalizer.from_xmi_id(generalization.general)
81
+ )
82
+ end
46
83
  end
47
84
 
48
85
  def common_args(element)
@@ -51,6 +88,7 @@ module Ea
51
88
  id: id,
52
89
  name: element.name,
53
90
  qualified_name: element.name,
91
+ stereotype: connector_stereotype(connector_index[id]),
54
92
  annotations: AnnotationBuilder.from_element(element, id)
55
93
  }
56
94
  end
@@ -88,8 +126,8 @@ module Ea
88
126
  target_multiplicity_raw: raw_card(target.type&.multiplicity),
89
127
  source_aggregation: source_aggregation(connector),
90
128
  target_aggregation: target.type&.aggregation || "none",
91
- source_navigable: navigable?(element, :source),
92
- target_navigable: navigable?(element, :target)
129
+ source_navigable: navigable?(element.id, :source),
130
+ target_navigable: navigable?(element.id, :target)
93
131
  }
94
132
  end
95
133
 
@@ -118,8 +156,8 @@ module Ea
118
156
  target_multiplicity_upper: parse_upper(target_end),
119
157
  source_aggregation: source_end&.aggregation || "none",
120
158
  target_aggregation: target_end&.aggregation || "none",
121
- source_navigable: navigable?(element, :source),
122
- target_navigable: navigable?(element, :target)
159
+ source_navigable: navigable?(element.id, :source),
160
+ target_navigable: navigable?(element.id, :target)
123
161
  }
124
162
  end
125
163
 
@@ -197,8 +235,8 @@ module Ea
197
235
  end
198
236
  end
199
237
 
200
- def navigable?(element, endpoint)
201
- connector = connector_index[element.id]
238
+ def navigable?(id, endpoint)
239
+ connector = connector_index[id]
202
240
  end_element = connector&.public_send(endpoint)
203
241
  flag = end_element&.modifiers&.is_navigable
204
242
  flag.nil? || flag
@@ -40,14 +40,24 @@ module Ea
40
40
 
41
41
  private
42
42
 
43
- # Text of the shape's UMLKeywordLabel child; name labels and
44
- # other labelled children never contribute a keyword.
43
+ # EA can save both a built-in keyword and a custom stereotype label.
44
+ # Prefer the label matching the element's saved stereotype, if present.
45
45
  def keyword_text(shape)
46
- label = shape.xpath("./ownedElement[@*[local-name()=\"type\"]=\"umldi:UMLKeywordLabel\"]").first
46
+ labels = shape.xpath("./ownedElement[@*[local-name()=\"type\"]=\"umldi:UMLKeywordLabel\"]")
47
+ stereotype = stereotypes_by_element[shape["modelElement"]]
48
+ label = labels.find { |candidate| stereotype && candidate["text"] == stereotype } || labels.first
47
49
  text = label && label["text"]
48
50
  text unless text.nil? || text.empty?
49
51
  end
50
52
 
53
+ def stereotypes_by_element
54
+ @stereotypes_by_element ||= doc.xpath("//*[local-name()=\"Extension\"]/elements/element")
55
+ .each_with_object({}) do |element, result|
56
+ stereotype = element.at_xpath("./properties")&.[]("stereotype")
57
+ result[element["idref"] || element["xmi:idref"]] = stereotype if stereotype
58
+ end
59
+ end
60
+
51
61
  def doc
52
62
  @doc ||= Nokogiri::XML(File.read(xmi_path))
53
63
  end
@@ -25,6 +25,7 @@ module Ea
25
25
  autoload :RelationshipBuilder, "ea/sources/xmi/relationship_builder"
26
26
  autoload :AnnotationBuilder, "ea/sources/xmi/annotation_builder"
27
27
  autoload :TagBuilder, "ea/sources/xmi/tag_builder"
28
+ autoload :LegendBuilder, "ea/sources/xmi/legend_builder"
28
29
  autoload :NoteBuilder, "ea/sources/xmi/note_builder"
29
30
  autoload :StereotypeBuilder, "ea/sources/xmi/stereotype_builder"
30
31
  autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
@@ -11,7 +11,9 @@ module Ea
11
11
  # boundaries to fit the element bounds.
12
12
  module NoteText
13
13
  X_PADDING = 5
14
+ HAND_DRAW_X_PADDING = 10
14
15
  Y_FIRST_OFFSET = 15
16
+ HAND_DRAW_Y_FIRST_OFFSET = 11
15
17
  LINE_OFFSET = 13
16
18
  CHAR_WIDTH_FACTOR = 0.55
17
19
 
@@ -24,23 +26,30 @@ module Ea
24
26
  notes = note_bodies(context.classifier)
25
27
  return nil if notes.empty?
26
28
 
27
- first_y = context.geometry.attr_first_y + Y_FIRST_OFFSET
29
+ x_padding, y_offset = note_offsets(context)
30
+ first_y = context.geometry.attr_first_y + y_offset
28
31
  bounds = context.bounds
29
32
  fill = context.theme.attribute_text_color
30
- usable_width = bounds.width - (X_PADDING * 2)
33
+ usable_width = bounds.width - (x_padding * 2)
31
34
  max_chars = (usable_width / (context.size * CHAR_WIDTH_FACTOR)).floor
32
35
 
33
36
  texts = []
34
37
  y = first_y
35
38
  notes.each do |body|
36
39
  wrapped_lines(body, max_chars).each do |line|
37
- texts << build_text(line, bounds.x + X_PADDING, y, context, fill)
40
+ texts << build_text(line, bounds.x + x_padding, y, context, fill)
38
41
  y += LINE_OFFSET
39
42
  end
40
43
  end
41
44
  wrap_group(texts.join("\n"), fill)
42
45
  end
43
46
 
47
+ # Native hand-drawn classifiers use a wider documentation inset.
48
+ def note_offsets(context)
49
+ context.diagram.hand_draw ? [HAND_DRAW_X_PADDING, HAND_DRAW_Y_FIRST_OFFSET] : [X_PADDING, Y_FIRST_OFFSET]
50
+ end
51
+ module_function :note_offsets
52
+
44
53
  def show_notes?(context)
45
54
  context.diagram && context.diagram.show_notes
46
55
  end
@@ -67,7 +76,9 @@ module Ea
67
76
  elsif (current + " " + word).length <= max_chars
68
77
  current = "#{current} #{word}"
69
78
  else
70
- lines << current
79
+ # EA retains the consumed word separator on a wrapped
80
+ # documentation row, including it in SVG textLength.
81
+ lines << "#{current} "
71
82
  current = word
72
83
  end
73
84
  end
@@ -90,8 +90,8 @@ module Ea
90
90
  end
91
91
 
92
92
  # Builds attribute display lines from classifier properties,
93
- # hiding properties that are navigable association ends
94
- # (those are rendered as connector lines).
93
+ # hiding association-end properties even when their connectors
94
+ # are not drawn on the diagram.
95
95
  #
96
96
  # When a property carries a stereotype (e.g. «voidable»),
97
97
  # EA renders the stereotype label as a separate line above
@@ -171,19 +171,11 @@ module Ea
171
171
  end
172
172
  private_class_method :build_text
173
173
 
174
- # Association-end properties render in the box only when
175
- # their association is NOT drawn on the diagram (drawn ones
176
- # render as connector role labels). exclude_association_ids
177
- # carries the diagram's drawn association ids; nil preserves
178
- # the legacy hide-all behavior.
179
- def self.displayable_properties(classifier, exclude_association_ids)
180
- return nil unless classifier.properties
181
-
182
- classifier.properties.reject do |prop|
183
- prop.association_id &&
184
- (exclude_association_ids.nil? ||
185
- exclude_association_ids.include?(prop.association_id))
186
- end
174
+ # Association ends remain in the model for connector role labels,
175
+ # but never become class-compartment attribute rows. The public
176
+ # exclude_association_ids keyword remains accepted for callers.
177
+ def self.displayable_properties(classifier, _exclude_association_ids)
178
+ classifier.properties&.reject(&:association_id)
187
179
  end
188
180
  private_class_method :displayable_properties
189
181
 
@@ -31,7 +31,7 @@ module Ea
31
31
  # @return [Array<String>] «name» labels (EA renders one
32
32
  # line per applied stereotype)
33
33
  def self.label_for(classifier, umldi_keyword, stereotype_fqname = nil)
34
- return ["«#{umldi_keyword}»"] if umldi_keyword && !umldi_keyword.empty?
34
+ return ["«#{canonical(umldi_keyword, stereotype_fqname)}»"] if umldi_keyword && !umldi_keyword.empty?
35
35
 
36
36
  explicit = explicit_stereotype(classifier, stereotype_fqname)
37
37
  return explicit if explicit
@@ -61,6 +61,8 @@ module Ea
61
61
  return [] unless context
62
62
 
63
63
  record_drawn_bounds(element, context)
64
+ return [] if element.style&.fetch("connector_proxy", false)
65
+
64
66
  Compartment.render_all(context).compact
65
67
  end
66
68
 
@@ -341,9 +341,21 @@ module Ea
341
341
  inset, baseline = property_role_offsets(direction)
342
342
  left = property_role_x(direction, x, inset + width)
343
343
  left = [left, MIN_ROLE_X].max if direction == :up
344
+ left += source_property_centering(connector, direction, box, role)
344
345
  [left, y + baseline]
345
346
  end
346
347
 
348
+ # Center a short role above the wider stereotype without moving
349
+ # the directional cluster's left edge. Other boxes keep their seat.
350
+ def source_property_centering(connector, direction, box, role)
351
+ return 0 unless %i[left down].include?(direction)
352
+ return 0 unless seat_font? && font_weight == 400 && font_style == "normal"
353
+ return 0 unless box.values_at("cy", "alignment") == [35, 1]
354
+
355
+ stereo = "«#{connector_source_dest_stereotype(connector, :source)}»"
356
+ -[stereotype_offset(role, stereo, box), 0].min
357
+ end
358
+
347
359
  def source_property_seat?(connector, direction, box)
348
360
  phantom_property_box?(connector, direction, box) ||
349
361
  (%i[up left right down].include?(direction) &&
@@ -885,8 +897,7 @@ module Ea
885
897
  # relationship guid), falling back to the property index
886
898
  # lookup by association source/target id.
887
899
  if connector&.relationship_ref
888
- placed = placed_classifier_id(association, end_kind)
889
- prop = assoc_end_property(placed, connector.relationship_ref)
900
+ prop = placed_end_property(association, end_kind, connector.relationship_ref)
890
901
  return prop if prop
891
902
  end
892
903
 
@@ -898,13 +909,27 @@ module Ea
898
909
  fallback_property_lookup(id)
899
910
  end
900
911
 
912
+ def placed_end_property(association, end_kind, relationship_ref)
913
+ placed = placed_classifier_id(association, end_kind)
914
+ property = assoc_end_property(placed, relationship_ref)
915
+ property unless property && opposite_role_property?(association, end_kind, property)
916
+ end
917
+
918
+ # A UML attribute belongs to the classifier opposite its role.
919
+ # Do not draw that same role again at its anonymous owner end.
920
+ def opposite_role_property?(association, end_kind, property)
921
+ own = end_kind == :source ? association.source_role_name : association.target_role_name
922
+ other = end_kind == :source ? association.target_role_name : association.source_role_name
923
+ role_empty?(own) && other == property.name
924
+ end
925
+
901
926
  def placed_classifier_id(association, end_kind)
902
927
  end_kind == :source ? association.source_id : association.target_id
903
928
  end
904
929
 
905
- # Find the synthesized association-end property on the
906
- # placed classifier whose association_id matches the
907
- # connector's relationship reference.
930
+ # Named association-end properties carry typed labels. Unnamed
931
+ # XMI member-end references defer to the saved association role
932
+ # and cardinality; direct property-id endpoints are resolved above.
908
933
  def assoc_end_property(classifier_id, relationship_ref)
909
934
  return nil unless classifier_id
910
935
 
@@ -912,7 +937,7 @@ module Ea
912
937
  return nil unless classifier.is_a?(Ea::Model::Classifier)
913
938
 
914
939
  (classifier.properties || []).find do |p|
915
- p.association_id == relationship_ref
940
+ p.association_id == relationship_ref && !p.name.to_s.empty?
916
941
  end
917
942
  end
918
943
 
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.133"
7
+ VERSION = "0.6.135"
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.133
4
+ version: 0.6.135
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -680,6 +680,7 @@ files:
680
680
  - lib/ea/model/note.rb
681
681
  - lib/ea/model/operation.rb
682
682
  - lib/ea/model/package.rb
683
+ - lib/ea/model/package_alias_index.rb
683
684
  - lib/ea/model/parameter.rb
684
685
  - lib/ea/model/point.rb
685
686
  - lib/ea/model/polymorphic_maps.rb
@@ -851,6 +852,9 @@ files:
851
852
  - lib/ea/sources/xmi/annotation_builder.rb
852
853
  - lib/ea/sources/xmi/classifier_builder.rb
853
854
  - lib/ea/sources/xmi/diagram_builder.rb
855
+ - lib/ea/sources/xmi/diagram_builder/connector_metadata.rb
856
+ - lib/ea/sources/xmi/diagram_builder/font_settings.rb
857
+ - lib/ea/sources/xmi/diagram_builder/stereotype_profiles.rb
854
858
  - lib/ea/sources/xmi/diagram_builder/umldi_edges.rb
855
859
  - lib/ea/sources/xmi/extension_elements.rb
856
860
  - lib/ea/sources/xmi/extension_geometry_parser.rb
@@ -858,6 +862,7 @@ files:
858
862
  - lib/ea/sources/xmi/extension_types.rb
859
863
  - lib/ea/sources/xmi/id_normalizer.rb
860
864
  - lib/ea/sources/xmi/instance_builder.rb
865
+ - lib/ea/sources/xmi/legend_builder.rb
861
866
  - lib/ea/sources/xmi/metadata_builder.rb
862
867
  - lib/ea/sources/xmi/note_builder.rb
863
868
  - lib/ea/sources/xmi/operation_builder.rb