ea 0.6.130 → 0.6.132
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 +4 -4
- data/lib/ea/fonts/metrics.rb +13 -4
- data/lib/ea/fonts/source_sans_pro_advances.json +1 -0
- data/lib/ea/model/diagram_connector.rb +7 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +8 -3
- data/lib/ea/sources/qea/diagram_builder.rb +26 -0
- data/lib/ea/sources/xmi/adapter.rb +33 -10
- data/lib/ea/sources/xmi/annotation_builder.rb +8 -0
- data/lib/ea/sources/xmi/classifier_builder.rb +11 -1
- data/lib/ea/sources/xmi/diagram_builder/umldi_edges.rb +48 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +63 -8
- data/lib/ea/sources/xmi/extension_geometry_parser.rb +7 -5
- data/lib/ea/sources/xmi/extension_style_parser.rb +6 -5
- data/lib/ea/sources/xmi/extension_types.rb +59 -0
- data/lib/ea/sources/xmi/instance_builder.rb +98 -0
- data/lib/ea/sources/xmi/operation_builder.rb +5 -5
- data/lib/ea/sources/xmi/reception_builder.rb +59 -0
- data/lib/ea/sources/xmi/relationship_builder.rb +61 -1
- data/lib/ea/sources/xmi/stored_route.rb +146 -0
- data/lib/ea/sources/xmi/type_classifier_map.rb +1 -0
- data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +10 -22
- data/lib/ea/sources/xmi.rb +4 -0
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +140 -0
- data/lib/ea/svg/ea_emitter/connector_redock.rb +36 -2
- data/lib/ea/svg/ea_emitter/connectors.rb +17 -36
- data/lib/ea/svg/ea_emitter/label/end_label.rb +58 -6
- data/lib/ea/svg/ea_emitter/labels.rb +24 -0
- data/lib/ea/svg/ea_emitter.rb +2 -0
- data/lib/ea/version.rb +1 -1
- metadata +9 -2
|
@@ -9,6 +9,8 @@ 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 :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
|
|
13
|
+
|
|
12
14
|
attr_reader :root, :xmi_path
|
|
13
15
|
|
|
14
16
|
def initialize(root, xmi_path = nil)
|
|
@@ -53,6 +55,7 @@ module Ea
|
|
|
53
55
|
bounds: canvas_bounds(placed_elements),
|
|
54
56
|
style: style_for(ext_diagram),
|
|
55
57
|
style_ex: style_ex_for(ext_diagram),
|
|
58
|
+
show_notes: style_ex_for(ext_diagram).to_s.include?("ShowNotes=1"),
|
|
56
59
|
elements: build_elements(placed_elements, id, umldi_keywords),
|
|
57
60
|
connectors: build_connectors(placed_connectors,
|
|
58
61
|
placed_elements, id),
|
|
@@ -144,20 +147,23 @@ module Ea
|
|
|
144
147
|
acc[pe.style.duid] = pe if pe.style.duid
|
|
145
148
|
end
|
|
146
149
|
|
|
147
|
-
placed_connectors.map
|
|
150
|
+
placed_connectors.each_with_index.map do |placed, index|
|
|
151
|
+
build_connector(placed, by_duid, owner_id, index)
|
|
152
|
+
end
|
|
148
153
|
end
|
|
149
154
|
|
|
150
|
-
def build_connector(placed, by_duid, owner_id)
|
|
155
|
+
def build_connector(placed, by_duid, owner_id, rank)
|
|
151
156
|
geom = placed.geometry
|
|
152
157
|
style = placed.style
|
|
153
158
|
source = by_duid[style.soid]
|
|
154
159
|
target = by_duid[style.eoid]
|
|
155
|
-
waypoints = compute_waypoints(geom, source, target)
|
|
156
160
|
ext_meta = connector_meta(placed.subject)
|
|
161
|
+
route = connector_route(placed, source, target, owner_id, ext_meta[:ea_type])
|
|
157
162
|
Ea::Model::DiagramConnector.new(
|
|
158
163
|
id: IdNormalizer.synthetic_id(owner_id, "conn", placed.subject),
|
|
159
164
|
diagram_id: owner_id,
|
|
160
165
|
relationship_ref: placed.subject,
|
|
166
|
+
paint_order: [rank],
|
|
161
167
|
source_duid: style.soid,
|
|
162
168
|
target_duid: style.eoid,
|
|
163
169
|
source_element_ref: source && synthetic_element_id(owner_id, source),
|
|
@@ -166,8 +172,8 @@ module Ea
|
|
|
166
172
|
target_edge: geom.edge,
|
|
167
173
|
connector_type: ext_meta[:ea_type],
|
|
168
174
|
direction: ext_meta[:direction],
|
|
169
|
-
waypoints: waypoints,
|
|
170
|
-
style:
|
|
175
|
+
waypoints: route[:waypoints],
|
|
176
|
+
style: route[:style],
|
|
171
177
|
line_color: style.color,
|
|
172
178
|
line_width: style.line_width,
|
|
173
179
|
hidden: style.hidden,
|
|
@@ -211,11 +217,35 @@ module Ea
|
|
|
211
217
|
IdNormalizer.synthetic_id(owner_id, "elem", placed.subject)
|
|
212
218
|
end
|
|
213
219
|
|
|
214
|
-
def
|
|
220
|
+
def connector_route(placed, source, target, owner_id, type)
|
|
221
|
+
native = umldi_waypoints(owner_id, placed) if placed.geometry.bend_points.to_a.empty?
|
|
222
|
+
points = native || compute_waypoints(placed.geometry, source, target,
|
|
223
|
+
style: placed.style, connector_type: type)
|
|
224
|
+
native ||= umldi_waypoints(owner_id, placed) if points.empty?
|
|
225
|
+
{ waypoints: native || points, style: native ? { umldi_route: true } : {} }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# EA's unique exported edge supplies bendless routes and recovers
|
|
229
|
+
# routes whose placement endpoints cannot be resolved.
|
|
230
|
+
def umldi_waypoints(diagram_id, placed)
|
|
231
|
+
return unless xmi_path
|
|
232
|
+
|
|
233
|
+
route = umldi_edges.route_for(diagram_id, placed.subject)
|
|
234
|
+
route&.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def umldi_edges
|
|
238
|
+
@umldi_edges ||= UmldiEdges.new(xmi_path)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def compute_waypoints(geom, source, target, style:, connector_type:)
|
|
215
242
|
src_bounds = bounds_from_placed(source)
|
|
216
243
|
tgt_bounds = bounds_from_placed(target)
|
|
217
244
|
return [] unless src_bounds && tgt_bounds
|
|
218
245
|
|
|
246
|
+
stored = stored_route(geom, style, connector_type, src_bounds, tgt_bounds)
|
|
247
|
+
return stored if stored
|
|
248
|
+
|
|
219
249
|
router = Ea::Svg::ConnectorRouter.new(
|
|
220
250
|
source_bounds: src_bounds,
|
|
221
251
|
target_bounds: tgt_bounds,
|
|
@@ -229,6 +259,27 @@ module Ea
|
|
|
229
259
|
end
|
|
230
260
|
end
|
|
231
261
|
|
|
262
|
+
# Stored bends are routed by StoredRoute where a QEA rule claims
|
|
263
|
+
# them; anything else keeps the generic router.
|
|
264
|
+
def stored_route(geom, style, connector_type, src_bounds, tgt_bounds)
|
|
265
|
+
offsets = { source: [geom.sx, geom.sy], target: [geom.ex, geom.ey] }
|
|
266
|
+
points = StoredRoute.new.waypoints(
|
|
267
|
+
link: StoredRoute::Link.new(style.raw, geom.path.to_s.tr("$", ";"), offset_geometry(geom)),
|
|
268
|
+
connector_type: connector_type, source: src_bounds, target: tgt_bounds,
|
|
269
|
+
bends: geom.bend_points.to_a, offsets: offsets, edge: geom.edge
|
|
270
|
+
)
|
|
271
|
+
return unless points
|
|
272
|
+
|
|
273
|
+
points.map do |x, y|
|
|
274
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def offset_geometry(geom)
|
|
279
|
+
{ "SX" => geom.sx, "SY" => geom.sy, "EX" => geom.ex, "EY" => geom.ey }
|
|
280
|
+
.filter_map { |key, value| "#{key}=#{value};" if value }.join
|
|
281
|
+
end
|
|
282
|
+
|
|
232
283
|
def bounds_from_placed(placed)
|
|
233
284
|
return nil unless placed
|
|
234
285
|
|
|
@@ -264,12 +315,16 @@ module Ea
|
|
|
264
315
|
|
|
265
316
|
label_boxes.transform_values do |box|
|
|
266
317
|
{
|
|
267
|
-
"cx" => box.cx, "cy" => box.cy,
|
|
268
318
|
"ox" => box.ox, "oy" => box.oy,
|
|
319
|
+
"cx" => box.cx, "cy" => box.cy,
|
|
320
|
+
"hidden" => box.hidden,
|
|
269
321
|
"bold" => box.bold,
|
|
270
322
|
"italic" => box.italic,
|
|
271
323
|
"underline" => box.underline,
|
|
272
|
-
"
|
|
324
|
+
"color" => box.color,
|
|
325
|
+
"alignment" => box.align,
|
|
326
|
+
"direction" => box.direction,
|
|
327
|
+
"rotation" => box.rotation
|
|
273
328
|
}
|
|
274
329
|
end
|
|
275
330
|
end
|
|
@@ -38,8 +38,9 @@ module Ea
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# EA's Path= field uses the format `x1:y1$x2:y2$...` where
|
|
41
|
-
# each pair is a bend point.
|
|
42
|
-
#
|
|
41
|
+
# each pair is a bend point. EA stores Y negated (as in QEA
|
|
42
|
+
# t_diagramlinks.Path); the result uses screen-positive Y,
|
|
43
|
+
# like element bounds.
|
|
43
44
|
def parse_bend_path(raw)
|
|
44
45
|
return [] if raw.nil? || raw.empty?
|
|
45
46
|
|
|
@@ -49,7 +50,7 @@ module Ea
|
|
|
49
50
|
x_str, y_str = pair.split(":", 2)
|
|
50
51
|
x = int(x_str)
|
|
51
52
|
y = int(y_str)
|
|
52
|
-
[x, y] if x && y
|
|
53
|
+
[x, -y] if x && y
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -72,7 +73,7 @@ module Ea
|
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
def extract_label_boxes(geometry_string)
|
|
75
|
-
geometry_string.to_s.scan(/\$?([A-Z]{3})=((?:[A-Za-z]{1,4}=-?\d
|
|
76
|
+
geometry_string.to_s.scan(/\$?([A-Z]{3})=((?:[A-Za-z]{1,4}=-?\d+:?)+)/).each_with_object({}) do |(key, body), acc|
|
|
76
77
|
sym = key.downcase.to_sym
|
|
77
78
|
next unless LABEL_BOX_KEYS.include?(sym)
|
|
78
79
|
next if body.nil? || body.empty?
|
|
@@ -98,6 +99,7 @@ module Ea
|
|
|
98
99
|
italic: hash["ITA"] == 1,
|
|
99
100
|
underline: hash["UND"] == 1,
|
|
100
101
|
hidden: hash["HDN"] == 1,
|
|
102
|
+
color: hash["CLR"],
|
|
101
103
|
align: hash["ALN"],
|
|
102
104
|
direction: hash["DIR"],
|
|
103
105
|
rotation: hash["ROT"]
|
|
@@ -114,7 +116,7 @@ module Ea
|
|
|
114
116
|
|
|
115
117
|
LabelBox = Struct.new(
|
|
116
118
|
:cx, :cy, :ox, :oy,
|
|
117
|
-
:bold, :italic, :underline, :hidden,
|
|
119
|
+
:bold, :italic, :underline, :hidden, :color,
|
|
118
120
|
:align, :direction, :rotation,
|
|
119
121
|
keyword_init: true
|
|
120
122
|
)
|
|
@@ -67,13 +67,14 @@ module Ea
|
|
|
67
67
|
value == "1" || value == "true" || value == "-1"
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# fontsz is EA's tenths of a point, rendered through GDI's double
|
|
71
|
+
# rounding (pixels = floor(tenths * 96 / 720), points =
|
|
72
|
+
# floor(pixels * 72 / 96)). Absent or zero means inherit, as nil.
|
|
70
73
|
def scale_font_size(value)
|
|
71
|
-
|
|
74
|
+
tenths = int(value)
|
|
75
|
+
return nil if tenths.nil? || tenths.zero?
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
return 13 if pct.nil? || pct.zero?
|
|
75
|
-
|
|
76
|
-
(pct * 13 / 100).round
|
|
77
|
+
(tenths * 96 / 720 * 72 / 96.0).floor
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
Style = Struct.new(
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
# Type names EA stores in the <xmi:Extension><elements> block,
|
|
9
|
+
# where the UML side only carries a primitive-type href or a
|
|
10
|
+
# placeholder idref. The xmi gem does not expose operations or
|
|
11
|
+
# parameters of the extension, so the raw XML is read once.
|
|
12
|
+
#
|
|
13
|
+
# <attribute xmi:idref="ID"><properties type="int"/>
|
|
14
|
+
# <operation xmi:idref="ID"><type type="void"/>
|
|
15
|
+
# <parameter xmi:idref="ID"><properties type="int"/>
|
|
16
|
+
class ExtensionTypes
|
|
17
|
+
def self.from_path(xmi_path)
|
|
18
|
+
return empty if xmi_path.nil?
|
|
19
|
+
|
|
20
|
+
new(Nokogiri::XML(File.read(xmi_path)))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.empty
|
|
24
|
+
new(nil)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(document)
|
|
28
|
+
@attributes = index(document, "attribute", "properties")
|
|
29
|
+
@returns = index(document, "operation", "type")
|
|
30
|
+
@parameters = index(document, "parameter", "properties")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def attribute_type(id)
|
|
34
|
+
@attributes[id]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def return_type(operation_id)
|
|
38
|
+
@returns[operation_id]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parameter_type(parameter_id)
|
|
42
|
+
@parameters[parameter_id]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def index(document, owner, child)
|
|
48
|
+
return {} unless document
|
|
49
|
+
|
|
50
|
+
document.xpath(%(//*[local-name()="#{owner}"][@*[local-name()="idref"]])).each_with_object({}) do |node, acc|
|
|
51
|
+
type = node.at_xpath(%(*[local-name()="#{child}"]/@type))&.value
|
|
52
|
+
idref = node.at_xpath("@*[local-name()='idref']").value
|
|
53
|
+
acc[idref] = type unless type.nil? || type.empty?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
# Builds Ea::Model::InstanceSpecification objects from
|
|
9
|
+
# uml:InstanceSpecification packaged elements. Slots come from
|
|
10
|
+
# the `runstate` attribute of the element's Extension entry,
|
|
11
|
+
# which carries slot names and operators that the typed
|
|
12
|
+
# `<slot>` children omit.
|
|
13
|
+
class InstanceBuilder
|
|
14
|
+
XMI_TYPE = "uml:InstanceSpecification"
|
|
15
|
+
RUNSTATE_XPATH =
|
|
16
|
+
%(//*[local-name()="element"]/*[local-name()="extendedProperties"][@runstate])
|
|
17
|
+
|
|
18
|
+
attr_reader :root, :xmi_path, :classifiers
|
|
19
|
+
|
|
20
|
+
def initialize(root, xmi_path, classifiers)
|
|
21
|
+
@root = root
|
|
22
|
+
@xmi_path = xmi_path
|
|
23
|
+
@classifiers = classifiers
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_all
|
|
27
|
+
instances = []
|
|
28
|
+
walk(root.model) do |element, package|
|
|
29
|
+
instances << build_one(element, package) if element.type == XMI_TYPE
|
|
30
|
+
end
|
|
31
|
+
instances
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def walk(element, package = nil, &block)
|
|
37
|
+
yield element, package
|
|
38
|
+
element.packaged_element.each do |inner|
|
|
39
|
+
walk(inner, inner.type == "uml:Package" ? inner : package, &block)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_one(element, package)
|
|
44
|
+
id = IdNormalizer.from_xmi_id(element.id)
|
|
45
|
+
properties = extension_properties[id]
|
|
46
|
+
Ea::Model::InstanceSpecification.new(
|
|
47
|
+
id: id,
|
|
48
|
+
name: element.name.to_s,
|
|
49
|
+
role_name: properties&.alias,
|
|
50
|
+
classifier_id: IdNormalizer.from_xmi_id(element.classifier),
|
|
51
|
+
classifier_name: classifier_names[element.classifier],
|
|
52
|
+
stereotype: properties&.stereotype.to_s,
|
|
53
|
+
package_id: package && IdNormalizer.from_xmi_id(package.id),
|
|
54
|
+
package_name: package&.name,
|
|
55
|
+
qualified_name: qualified_name(element, package),
|
|
56
|
+
slots: slots_for(id),
|
|
57
|
+
annotations: AnnotationBuilder.from_element(element, id) +
|
|
58
|
+
AnnotationBuilder.from_documentation(properties&.documentation, id)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def qualified_name(element, package)
|
|
63
|
+
name = element.name.to_s
|
|
64
|
+
return name if name.empty? || package.nil?
|
|
65
|
+
|
|
66
|
+
"#{package.name}::#{name}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def classifier_names
|
|
70
|
+
@classifier_names ||= classifiers.to_h { |c| [c.id, c.name.to_s] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extension_properties
|
|
74
|
+
@extension_properties ||=
|
|
75
|
+
(root.extension&.elements&.element || []).to_h { |e| [e.idref, e.properties] }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def slots_for(id)
|
|
79
|
+
runstates[id].to_s.scan(Ea::Sources::Qea::InstanceBuilder::RUN_STATE_PATTERN).map do |(name, value, op)|
|
|
80
|
+
Ea::Model::Slot.new(name: name.to_s, value: value.to_s, op: op.to_s.empty? ? "=" : op)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def runstates
|
|
85
|
+
@runstates ||= extension_runstates
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def extension_runstates
|
|
89
|
+
return {} if xmi_path.nil?
|
|
90
|
+
|
|
91
|
+
Nokogiri::XML(File.read(xmi_path))
|
|
92
|
+
.xpath(RUNSTATE_XPATH)
|
|
93
|
+
.to_h { |node| [node.parent["xmi:idref"], node["runstate"]] }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -19,7 +19,7 @@ module Ea
|
|
|
19
19
|
owner_id: owner.id,
|
|
20
20
|
qualified_name: "#{owner.name}::#{op.name}",
|
|
21
21
|
return_type_name: return_type_name_for(op),
|
|
22
|
-
visibility: op.visibility,
|
|
22
|
+
visibility: op.visibility || "public",
|
|
23
23
|
is_static: boolean(op.is_static),
|
|
24
24
|
is_abstract: boolean(op.is_abstract),
|
|
25
25
|
parameters: build_parameters(op, id),
|
|
@@ -30,21 +30,21 @@ module Ea
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def build_parameters(op, owner_op_id)
|
|
33
|
-
op.owned_parameter.map.with_index do |param, idx|
|
|
34
|
-
direction = param.direction ||
|
|
33
|
+
op.owned_parameter.reject { |p| p.direction == "return" }.map.with_index do |param, idx|
|
|
34
|
+
direction = param.direction || "in"
|
|
35
35
|
Ea::Model::Parameter.new(
|
|
36
36
|
id: IdNormalizer.from_xmi_id(param.id) ||
|
|
37
37
|
IdNormalizer.synthetic_id(owner_op_id, "param", idx.to_s),
|
|
38
38
|
name: param.name || direction,
|
|
39
39
|
ordinal: idx,
|
|
40
40
|
direction: direction,
|
|
41
|
-
type_name: param.
|
|
41
|
+
type_name: param.classifier_type
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def return_type_name_for(op)
|
|
47
|
-
op.owned_parameter.find { |p| p.direction == "return" }&.
|
|
47
|
+
op.owned_parameter.find { |p| p.direction == "return" }&.classifier_type
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def boolean(value)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
# The xmi gem omits ownedReception, so read those source members
|
|
9
|
+
# directly rather than inferring operations from extension rows.
|
|
10
|
+
class ReceptionBuilder
|
|
11
|
+
def initialize(xmi_path)
|
|
12
|
+
@xmi_path = xmi_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def grouped_by_owner
|
|
16
|
+
return {} unless @xmi_path
|
|
17
|
+
|
|
18
|
+
@grouped_by_owner ||= read_receptions(Nokogiri::XML(File.read(@xmi_path)))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def read_receptions(document)
|
|
24
|
+
scopes = extension_scopes(document)
|
|
25
|
+
xpath = '//*[local-name()="Model"]//*[local-name()="packagedElement"]/*[local-name()="ownedReception"]'
|
|
26
|
+
document.xpath(xpath).each_with_object({}) do |node, grouped|
|
|
27
|
+
owner_id = xmi_attribute(node.parent, "id")
|
|
28
|
+
next unless owner_id
|
|
29
|
+
|
|
30
|
+
(grouped[owner_id] ||= []) << build_one(node, owner_id, scopes)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_one(node, owner_id, scopes)
|
|
35
|
+
id = xmi_attribute(node, "id") ||
|
|
36
|
+
IdNormalizer.synthetic_id(owner_id, "reception", node["name"])
|
|
37
|
+
Ea::Model::Operation.new(
|
|
38
|
+
id: id, name: node["name"], owner_id: owner_id,
|
|
39
|
+
qualified_name: "#{node.parent["name"]}::#{node["name"]}",
|
|
40
|
+
is_reception: true,
|
|
41
|
+
visibility: node["visibility"] || scopes[id] || "public"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extension_scopes(document)
|
|
46
|
+
xpath = '//*[local-name()="Extension"]//*[local-name()="receptions"]/*[local-name()="reception"]'
|
|
47
|
+
document.xpath(xpath).each_with_object({}) do |node, scopes|
|
|
48
|
+
id = xmi_attribute(node, "idref")
|
|
49
|
+
scopes[id] = node["scope"].downcase if id && node["scope"]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def xmi_attribute(node, name)
|
|
54
|
+
node.at_xpath("@*[local-name()='#{name}']")&.value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -7,6 +7,13 @@ module Ea
|
|
|
7
7
|
# uml:Generalization, uml:Realization, uml:Dependency) into
|
|
8
8
|
# the appropriate Ea::Model::Relationship subclass.
|
|
9
9
|
class RelationshipBuilder
|
|
10
|
+
# Connector types that imply an aggregation on the source end
|
|
11
|
+
# when the end itself records none.
|
|
12
|
+
AGGREGATION_FOR_CONNECTOR_TYPE = {
|
|
13
|
+
"Aggregation" => "shared",
|
|
14
|
+
"Composition" => "composite"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
10
17
|
attr_reader :root
|
|
11
18
|
|
|
12
19
|
def initialize(root)
|
|
@@ -42,7 +49,7 @@ module Ea
|
|
|
42
49
|
id = IdNormalizer.from_xmi_id(element.id)
|
|
43
50
|
{
|
|
44
51
|
id: id,
|
|
45
|
-
name: element.name
|
|
52
|
+
name: element.name,
|
|
46
53
|
qualified_name: element.name,
|
|
47
54
|
annotations: AnnotationBuilder.from_element(element, id)
|
|
48
55
|
}
|
|
@@ -59,6 +66,41 @@ module Ea
|
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
def association_args(element)
|
|
69
|
+
connector = connector_index[element.id]
|
|
70
|
+
return owned_end_args(element) unless connector&.source && connector.target
|
|
71
|
+
|
|
72
|
+
connector_end_args(element, connector)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def connector_end_args(element, connector)
|
|
76
|
+
source = connector.source
|
|
77
|
+
target = connector.target
|
|
78
|
+
{
|
|
79
|
+
source_id: source.idref,
|
|
80
|
+
target_id: target.idref,
|
|
81
|
+
source_role_name: source.role&.name,
|
|
82
|
+
target_role_name: target.role&.name,
|
|
83
|
+
source_multiplicity_lower: card_lower(source.type&.multiplicity),
|
|
84
|
+
source_multiplicity_upper: card_upper(source.type&.multiplicity),
|
|
85
|
+
target_multiplicity_lower: card_lower(target.type&.multiplicity),
|
|
86
|
+
target_multiplicity_upper: card_upper(target.type&.multiplicity),
|
|
87
|
+
source_multiplicity_raw: raw_card(source.type&.multiplicity),
|
|
88
|
+
target_multiplicity_raw: raw_card(target.type&.multiplicity),
|
|
89
|
+
source_aggregation: source_aggregation(connector),
|
|
90
|
+
target_aggregation: target.type&.aggregation || "none",
|
|
91
|
+
source_navigable: navigable?(element, :source),
|
|
92
|
+
target_navigable: navigable?(element, :target)
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def source_aggregation(connector)
|
|
97
|
+
flagged = connector.source.type&.aggregation || "none"
|
|
98
|
+
return flagged unless flagged == "none"
|
|
99
|
+
|
|
100
|
+
AGGREGATION_FOR_CONNECTOR_TYPE.fetch(connector.properties&.ea_type, "none")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def owned_end_args(element)
|
|
62
104
|
ends = element.member_ends.to_a
|
|
63
105
|
source_id = ends.first&.idref
|
|
64
106
|
target_id = ends.last&.idref
|
|
@@ -81,6 +123,24 @@ module Ea
|
|
|
81
123
|
}
|
|
82
124
|
end
|
|
83
125
|
|
|
126
|
+
def raw_card(cardinality)
|
|
127
|
+
text = cardinality.to_s.strip
|
|
128
|
+
text.empty? ? nil : text
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def card_lower(cardinality)
|
|
132
|
+
text = raw_card(cardinality)
|
|
133
|
+
Integer(text.split("..").first, exception: false) if text
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def card_upper(cardinality)
|
|
137
|
+
text = raw_card(cardinality)
|
|
138
|
+
return nil unless text
|
|
139
|
+
|
|
140
|
+
upper = text.split("..").last
|
|
141
|
+
upper == "*" ? -1 : Integer(upper, exception: false)
|
|
142
|
+
end
|
|
143
|
+
|
|
84
144
|
def generalization_args(element)
|
|
85
145
|
# PackagedElement#generalization is a collection of
|
|
86
146
|
# AssociationGeneralization rows. The "specific" (child)
|