ea 0.6.39 → 0.6.41
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/README.adoc +36 -0
- data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +30 -0
- data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +6 -3
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +9 -4
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +13 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/loader.rb +76 -0
- data/lib/ea/xmi/slicer.rb +412 -0
- data/lib/ea/xmi.rb +23 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 648d534db9bc161659ceb4fb001a91dd027c2de38714d64b757ade37728c9608
|
|
4
|
+
data.tar.gz: ad4addbbbc6e2695b44df97431779acd1cf561d2f4e42c20f64dca60eae11230
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c0d526857ccf9c8c78eca78e954f8cc4332c7abaa8311c53559ded2bb78dbee599e7e2ec800f997108310078f58115e269bee8aed700295922f08b196fd96f8
|
|
7
|
+
data.tar.gz: 63def8e36e609ff0b1a7dc735ae27ae4518de1394009d1c847ecea14da6451ae3e32e40d90c5f8d00bc9795262bc7f85615a1575c23132f0ff46c42dca53e9c7
|
data/README.adoc
CHANGED
|
@@ -35,6 +35,42 @@ db = Ea.parse("model.qea") #=> Ea::Qea::Database
|
|
|
35
35
|
puts "#{db.packages.size} packages, #{db.objects.size} objects"
|
|
36
36
|
----
|
|
37
37
|
|
|
38
|
+
=== Whole and partial XMI loading (pure — no lutaml-uml needed)
|
|
39
|
+
|
|
40
|
+
`Ea::Xmi.load` parses an EA XMI export into the typed Sparx model.
|
|
41
|
+
Whole load is the default and parses the entire file:
|
|
42
|
+
|
|
43
|
+
[source,ruby]
|
|
44
|
+
----
|
|
45
|
+
require "ea/xmi"
|
|
46
|
+
|
|
47
|
+
model = Ea::Xmi.load("export.xmi")
|
|
48
|
+
----
|
|
49
|
+
|
|
50
|
+
Partial load parses only the reference closure of the named elements.
|
|
51
|
+
The wanted pairs are `[package, name]`; a nil package matches by name
|
|
52
|
+
in any package. The closure is assembled in memory by
|
|
53
|
+
`Ea::Xmi::Slicer` — the export is streamed once, no temporary files
|
|
54
|
+
are written, and the parse runs over the slice:
|
|
55
|
+
|
|
56
|
+
[source,ruby]
|
|
57
|
+
----
|
|
58
|
+
model = Ea::Xmi.load("export.xmi", partial: [["bldg", "Building"]])
|
|
59
|
+
----
|
|
60
|
+
|
|
61
|
+
Partial load produces the same content as whole load for everything
|
|
62
|
+
derived from the model's elements: the slice keeps each wanted class
|
|
63
|
+
with its attributes, operations, connectors, generalization parents,
|
|
64
|
+
package ancestry, and the diagrams that reference kept elements. Use
|
|
65
|
+
it when reading a handful of classes out of a large export — a
|
|
66
|
+
100 MB export parses as a few MB slice instead of a 10+ GB object
|
|
67
|
+
graph.
|
|
68
|
+
|
|
69
|
+
`Ea::Xmi.load_graph` wraps the same two modes one layer up, returning
|
|
70
|
+
the parsed EA graph: the parser, the parsed document, and the lookup
|
|
71
|
+
indexes the table renders resolve through
|
|
72
|
+
(`Ea::Xmi::Loaded`).
|
|
73
|
+
|
|
38
74
|
=== Generate a SPA from QEA (requires lutaml-uml)
|
|
39
75
|
|
|
40
76
|
[source,bash]
|
|
@@ -37,6 +37,36 @@ module Ea
|
|
|
37
37
|
context = Context.new(classifier: classifier, **opts)
|
|
38
38
|
PROVIDERS.flat_map { |provider| provider.call(context) }
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
# Greedy word wrap measured with the header's own font.
|
|
42
|
+
# EA wraps overflowing headers at word boundaries
|
|
43
|
+
# (81F92FC7: instance header "new ownership: LA_Right"
|
|
44
|
+
# renders as "new ownership:" / "LA_Right").
|
|
45
|
+
def self.wrap_words(text, context, weight)
|
|
46
|
+
font_style = weight == :bold_italic ? "italic" : "normal"
|
|
47
|
+
width = lambda do |s|
|
|
48
|
+
TextRenderer.estimate_width(
|
|
49
|
+
s, context.font_size || 9, nil,
|
|
50
|
+
family: context.family, weight: "700", style: font_style
|
|
51
|
+
).round
|
|
52
|
+
end
|
|
53
|
+
return [text] if context.bounds_width.nil?
|
|
54
|
+
return [text] if width.call(text) <= context.bounds_width.to_i
|
|
55
|
+
|
|
56
|
+
lines = []
|
|
57
|
+
current = +""
|
|
58
|
+
text.split(" ").each do |word|
|
|
59
|
+
candidate = current.empty? ? word : "#{current} #{word}"
|
|
60
|
+
if !current.empty? && width.call(candidate) > context.bounds_width.to_i
|
|
61
|
+
lines << current
|
|
62
|
+
current = word
|
|
63
|
+
else
|
|
64
|
+
current = candidate
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
lines << current unless current.empty?
|
|
68
|
+
lines
|
|
69
|
+
end
|
|
40
70
|
end
|
|
41
71
|
end
|
|
42
72
|
end
|
|
@@ -6,8 +6,9 @@ module Ea
|
|
|
6
6
|
module Element
|
|
7
7
|
module HeaderLineProvider
|
|
8
8
|
# InstanceSpecification: short-circuits the pipeline. The
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# header for an instance is "name: Classifier" on one bold
|
|
10
|
+
# line, word-wrapped when it overflows the box (81F92FC7:
|
|
11
|
+
# "new ownership:" / "LA_Right").
|
|
11
12
|
class InstanceSpec
|
|
12
13
|
def self.call(context)
|
|
13
14
|
inst = context.classifier
|
|
@@ -17,7 +18,9 @@ module Ea
|
|
|
17
18
|
if inst.classifier_name && !inst.classifier_name.empty?
|
|
18
19
|
label = "#{label}: #{inst.classifier_name}"
|
|
19
20
|
end
|
|
20
|
-
|
|
21
|
+
HeaderLinePipeline.wrap_words(label, context, :bold).map do |line|
|
|
22
|
+
[line, :bold]
|
|
23
|
+
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -38,13 +38,18 @@ module Ea
|
|
|
38
38
|
def self.wrapped_name_lines(classifier, context, weight)
|
|
39
39
|
name = display_name(classifier)
|
|
40
40
|
name = "#{context.foreign_package_name}::#{name}" if context.foreign_package_name
|
|
41
|
-
unless context.bounds_width &&
|
|
42
|
-
name_exceeds_bounds?(name, context, weight)
|
|
41
|
+
unless context.bounds_width && name_exceeds_bounds?(name, context, weight)
|
|
43
42
|
return [[name, weight]]
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if name.include?("::")
|
|
46
|
+
qualifier, base = name.split("::", 2)
|
|
47
|
+
[["#{qualifier}::", weight], [base, weight]]
|
|
48
|
+
else
|
|
49
|
+
HeaderLinePipeline.wrap_words(name, context, weight).map do |line|
|
|
50
|
+
[line, weight]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def self.name_exceeds_bounds?(name, context, weight)
|
|
@@ -33,6 +33,17 @@ module Ea
|
|
|
33
33
|
return nil if content.nil?
|
|
34
34
|
|
|
35
35
|
mx, my = midpoint(points)
|
|
36
|
+
# EA splits a "name: value" label at the colon-space into
|
|
37
|
+
# two lines centered on the same slot (81F92FC7: "new
|
|
38
|
+
# ownership:" / "LA_Right" at y 172/185, both centered on
|
|
39
|
+
# x=480).
|
|
40
|
+
if (idx = content.index(": ")) && content.length > idx + 2
|
|
41
|
+
return [
|
|
42
|
+
render_text(mx, my, content[0..idx]),
|
|
43
|
+
render_text(mx, my + LINE_PITCH, content[(idx + 2)..])
|
|
44
|
+
].join("\n")
|
|
45
|
+
end
|
|
46
|
+
|
|
36
47
|
render_text(mx, my, content)
|
|
37
48
|
end
|
|
38
49
|
|
|
@@ -74,6 +85,8 @@ module Ea
|
|
|
74
85
|
|
|
75
86
|
private
|
|
76
87
|
|
|
88
|
+
LINE_PITCH = 13
|
|
89
|
+
|
|
77
90
|
def midpoint(points)
|
|
78
91
|
mid_idx = points.size / 2
|
|
79
92
|
if points.size.odd?
|
data/lib/ea/version.rb
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "xmi"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Xmi
|
|
7
|
+
# The result of loading an export into the EA graph: the parser,
|
|
8
|
+
# the parsed document, and the indexes the consumers resolve
|
|
9
|
+
# through.
|
|
10
|
+
Loaded = Struct.new(:parser, :uml_document, :drop_options,
|
|
11
|
+
:xmi_id_index, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
module Loader
|
|
14
|
+
# Whole load parses the entire export. Partial load parses only
|
|
15
|
+
# the reference closure of the wanted [package, name] pairs,
|
|
16
|
+
# assembled in memory by Ea::Xmi::Slicer - no temporary files.
|
|
17
|
+
# Both modes produce identical content for everything derived
|
|
18
|
+
# from the model's elements; partial load exists so a consumer
|
|
19
|
+
# reading a handful of classes out of a large export never has
|
|
20
|
+
# to hydrate the whole of it. package may be nil, matching by
|
|
21
|
+
# name in any package.
|
|
22
|
+
def self.call(source, partial: nil)
|
|
23
|
+
xml = read(source)
|
|
24
|
+
xml = Slicer.slice(xml, partial) if partial
|
|
25
|
+
::Xmi::Sparx::Root.parse_xml(xml)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.graph(source, partial: nil)
|
|
29
|
+
parser = Parser.new
|
|
30
|
+
uml_document = parser.parse(call(source, partial: partial))
|
|
31
|
+
Loaded.new(
|
|
32
|
+
parser: parser,
|
|
33
|
+
uml_document: uml_document,
|
|
34
|
+
drop_options: build_drop_options(parser),
|
|
35
|
+
xmi_id_index: build_xmi_id_index(uml_document),
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.read(source)
|
|
40
|
+
source.respond_to?(:read) ? source.read : File.read(source.to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.build_drop_options(parser)
|
|
44
|
+
lookup = LookupService.new(parser)
|
|
45
|
+
{
|
|
46
|
+
xmi_root_model: parser.xmi_root_model,
|
|
47
|
+
id_name_mapping: parser.id_name_mapping,
|
|
48
|
+
lookup: lookup,
|
|
49
|
+
with_gen: true,
|
|
50
|
+
with_assoc: true,
|
|
51
|
+
with_absolute_path: true,
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# One xmi_id -> node walk per parse.
|
|
56
|
+
def self.build_xmi_id_index(uml_document)
|
|
57
|
+
index = {}
|
|
58
|
+
collect = lambda do |container|
|
|
59
|
+
index[container.xmi_id] ||= container if container.respond_to?(:xmi_id)
|
|
60
|
+
if container.respond_to?(:classes)
|
|
61
|
+
container.classes.each { |n| index[n.xmi_id] ||= n }
|
|
62
|
+
end
|
|
63
|
+
if container.respond_to?(:data_types)
|
|
64
|
+
container.data_types.each { |n| index[n.xmi_id] ||= n }
|
|
65
|
+
end
|
|
66
|
+
if container.respond_to?(:enums)
|
|
67
|
+
container.enums.each { |n| index[n.xmi_id] ||= n }
|
|
68
|
+
end
|
|
69
|
+
(container.packages || []).each { |p| collect.call(p) } if container.respond_to?(:packages)
|
|
70
|
+
end
|
|
71
|
+
collect.call(uml_document)
|
|
72
|
+
index
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "moxml"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Ea
|
|
7
|
+
module Xmi
|
|
8
|
+
# Prunes an EA XMI export to the subgraphs reachable from sets of
|
|
9
|
+
# (package name, element name) pairs, so the existing parse pipeline
|
|
10
|
+
# hydrates small slices instead of the whole export. A plateau-scale
|
|
11
|
+
# export is 98 MB, more than half of it diagram presentation data
|
|
12
|
+
# that no table reads, and materializing the whole Xmi tree plus the
|
|
13
|
+
# Ea graph costs 10+ GB of live objects.
|
|
14
|
+
#
|
|
15
|
+
# The closure is generic: every attribute value that looks like an EA
|
|
16
|
+
# element id (EAID_/EAPK_/EAEM_) found anywhere inside a kept
|
|
17
|
+
# element's subtree keeps the referenced element too, transitively -
|
|
18
|
+
# no XMI semantics are encoded here. Only container tags are
|
|
19
|
+
# membership-gated (packagedElement in the model, element and
|
|
20
|
+
# connector in the extension); anything nested inside a kept
|
|
21
|
+
# container is copied verbatim. The diagrams section is dropped.
|
|
22
|
+
#
|
|
23
|
+
# `slices` performs one index pass and one write pass and emits one
|
|
24
|
+
# standalone slice per requested group, so each consumer (one macro,
|
|
25
|
+
# one class table) hydrates only its own class's subgraph: the
|
|
26
|
+
# per-parse peak is the size of one slice, not of the export.
|
|
27
|
+
class Slicer
|
|
28
|
+
EA_ID_RE = /\A(?:EAID|EAPK|EAEM)_[0-9A-Za-z_]+\z/
|
|
29
|
+
private_constant :EA_ID_RE
|
|
30
|
+
|
|
31
|
+
XML_DECL = %(<?xml version="1.0" encoding="UTF-8"?>\n)
|
|
32
|
+
private_constant :XML_DECL
|
|
33
|
+
|
|
34
|
+
# SAX passes over ~100 MB EA exports fire millions of events whose
|
|
35
|
+
# per-event strings and hashes outrun the collector (malloc grows
|
|
36
|
+
# for GBs before an automatic cycle). Collect at a fixed event
|
|
37
|
+
# cadence once the malloc budget is blown.
|
|
38
|
+
GC_BUDGET_BYTES = 512 * 1024 * 1024
|
|
39
|
+
private_constant :GC_BUDGET_BYTES
|
|
40
|
+
|
|
41
|
+
def self.gc_when_bloated
|
|
42
|
+
GC.start if GC.stat(:malloc_increase_bytes) > GC_BUDGET_BYTES
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Containers whose membership is gated against the keep set, by
|
|
46
|
+
# document section. umldi:Diagram carries the embedded diagram
|
|
47
|
+
# shapes (tens of MB with waypoints and bounds in an EA export)
|
|
48
|
+
# and is never referenced by table content, so it gates on the
|
|
49
|
+
# keep set as well.
|
|
50
|
+
GATED = {
|
|
51
|
+
model: ["packagedElement", "umldi:Diagram"].freeze,
|
|
52
|
+
elements: ["element"].freeze,
|
|
53
|
+
connectors: ["connector"].freeze,
|
|
54
|
+
diagrams: ["diagram"].freeze,
|
|
55
|
+
}.freeze
|
|
56
|
+
private_constant :GATED
|
|
57
|
+
|
|
58
|
+
SECTION_CHILD = {
|
|
59
|
+
"elements" => :elements,
|
|
60
|
+
"connectors" => :connectors,
|
|
61
|
+
"diagrams" => :drop,
|
|
62
|
+
"profiles" => :drop,
|
|
63
|
+
}.freeze
|
|
64
|
+
private_constant :SECTION_CHILD
|
|
65
|
+
|
|
66
|
+
Node = Struct.new(:id, :tag, :xmi_type, :name, :parent_id, :section,
|
|
67
|
+
:refs, :links_refs, keyword_init: true)
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# Single slice for one wanted set, as a string: the in-memory
|
|
71
|
+
# form partial loading consumes.
|
|
72
|
+
# wanted: array of [package_name, element_name]; package may be
|
|
73
|
+
# nil, in which case elements match by name in any package.
|
|
74
|
+
def slice(source, wanted)
|
|
75
|
+
new(source).single(wanted)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# One index pass, one write pass, one standalone slice per
|
|
79
|
+
# group, returned as strings. groups: hash key => wanted array
|
|
80
|
+
# (same shape as +wanted+ above). Groups whose wanted set
|
|
81
|
+
# matches nothing are skipped entirely, so callers can fall
|
|
82
|
+
# back to the full source for them.
|
|
83
|
+
def slices(source, groups)
|
|
84
|
+
new(source).multi(groups)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def initialize(source)
|
|
89
|
+
@source = source
|
|
90
|
+
@moxml = Moxml.new(:leptris)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def single(wanted)
|
|
94
|
+
nodes = index_pass
|
|
95
|
+
keep = closure(nodes, seed_ids(nodes, wanted))
|
|
96
|
+
output = StringIO.new(+"")
|
|
97
|
+
writer = GroupWriter.new([keep], [output])
|
|
98
|
+
@moxml.sax_parse(source_string, writer)
|
|
99
|
+
output.string
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def multi(groups)
|
|
103
|
+
nodes = index_pass
|
|
104
|
+
keeps = []
|
|
105
|
+
keys = []
|
|
106
|
+
groups.each do |key, wanted|
|
|
107
|
+
seeds = seed_ids(nodes, wanted)
|
|
108
|
+
next if seeds.empty?
|
|
109
|
+
|
|
110
|
+
keeps << closure(nodes, seeds)
|
|
111
|
+
keys << key
|
|
112
|
+
end
|
|
113
|
+
outputs = keys.map { StringIO.new(+"") }
|
|
114
|
+
@moxml.sax_parse(source_string, GroupWriter.new(keeps, outputs))
|
|
115
|
+
keys.zip(outputs).to_h { |k, o| [k, o.string] }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# --- pass one: index id-bearing containers and their references --
|
|
121
|
+
|
|
122
|
+
class IndexHandler < ::Moxml::SAX::Handler
|
|
123
|
+
attr_reader :nodes
|
|
124
|
+
|
|
125
|
+
def initialize
|
|
126
|
+
@nodes = {}
|
|
127
|
+
@stack = [] # [id_or_nil, shared_refs_array]
|
|
128
|
+
@section = []
|
|
129
|
+
@refs_off = 0
|
|
130
|
+
@events = 0
|
|
131
|
+
@links_depth = 0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Only gated containers become nodes (packagedElement in the
|
|
135
|
+
# model, element and connector in the extension, and the
|
|
136
|
+
# embedded umldi:Diagram blocks). Other id-bearing elements -
|
|
137
|
+
# `<type xmi:idref>`, `<model package>` and the like - are pure
|
|
138
|
+
# references: recording them as nodes would let a reference
|
|
139
|
+
# define the element's ancestry, so their EA-id values flow into
|
|
140
|
+
# the enclosing container's reference set instead.
|
|
141
|
+
def on_start_element(name, attributes = {}, _namespaces = {})
|
|
142
|
+
Slicer.gc_when_bloated if (@events += 1) % 50_000 == 0
|
|
143
|
+
@section << section_for(name, @section.last)
|
|
144
|
+
@refs_off += 1 if name == "links"
|
|
145
|
+
@links_depth += 1 if name == "links"
|
|
146
|
+
id = attributes["xmi:id"] || attributes["xmi:idref"]
|
|
147
|
+
is_node = id && GATED.fetch(@section.last, []).include?(name)
|
|
148
|
+
if is_node && (existing = @nodes[id])
|
|
149
|
+
# A container id occurs twice in an EA export: as the model
|
|
150
|
+
# element (uml:Model subtree) and as the extension entry
|
|
151
|
+
# keyed by xmi:idref. Merge so one node carries the model
|
|
152
|
+
# ancestry and both occurrences' references; the entry
|
|
153
|
+
# usually carries the name.
|
|
154
|
+
existing.name ||= attributes["name"]
|
|
155
|
+
existing.xmi_type ||= attributes["xmi:type"]
|
|
156
|
+
collect_refs(attributes, existing.refs)
|
|
157
|
+
@stack << [id, existing.refs]
|
|
158
|
+
return
|
|
159
|
+
end
|
|
160
|
+
# the parent is the nearest id-bearing element already on the
|
|
161
|
+
# stack, so it has to be read before this element is pushed
|
|
162
|
+
parent = nearest_id
|
|
163
|
+
refs = is_node ? [] : (@stack.last ? @stack.last[1] : [])
|
|
164
|
+
@stack << [is_node ? id : nil, refs]
|
|
165
|
+
unless is_node
|
|
166
|
+
collect_refs(attributes, refs)
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
node = Node.new(
|
|
171
|
+
id: id, tag: name,
|
|
172
|
+
xmi_type: attributes["xmi:type"], name: attributes["name"],
|
|
173
|
+
parent_id: parent, section: @section.last, refs: refs,
|
|
174
|
+
links_refs: [],
|
|
175
|
+
)
|
|
176
|
+
@nodes[id] = node
|
|
177
|
+
collect_refs(attributes, node.refs)
|
|
178
|
+
collect_refs(attributes, node.links_refs) if @links_depth.positive?
|
|
179
|
+
node
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def on_end_element(name)
|
|
183
|
+
@refs_off -= 1 if name == "links"
|
|
184
|
+
@links_depth -= 1 if name == "links" && @links_depth.positive?
|
|
185
|
+
@stack.pop
|
|
186
|
+
@section.pop
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def nearest_id
|
|
192
|
+
@stack.reverse_each do |id, _|
|
|
193
|
+
return id if id
|
|
194
|
+
end
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def section_for(name, current)
|
|
199
|
+
return :model if name == "uml:Model"
|
|
200
|
+
return :extension if name == "xmi:Extension"
|
|
201
|
+
|
|
202
|
+
if current == :extension
|
|
203
|
+
SECTION_CHILD[name] || :drop
|
|
204
|
+
elsif current == :drop
|
|
205
|
+
:drop
|
|
206
|
+
else
|
|
207
|
+
current
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The links blocks inside extension entries restate every
|
|
212
|
+
# relation's endpoints; the connector records in the connectors
|
|
213
|
+
# section already carry both ends, so collecting from links
|
|
214
|
+
# would pull the endpoint classes of every relation into the
|
|
215
|
+
# closure transitively (half the model per class).
|
|
216
|
+
def collect_refs(attributes, refs)
|
|
217
|
+
return if @refs_off.positive?
|
|
218
|
+
|
|
219
|
+
attributes.each_value do |v|
|
|
220
|
+
refs << v if v.is_a?(String) && v.match?(EA_ID_RE)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def index_pass
|
|
226
|
+
handler = IndexHandler.new
|
|
227
|
+
@moxml.sax_parse(source_string, handler)
|
|
228
|
+
handler.nodes
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def source_string
|
|
232
|
+
case @source
|
|
233
|
+
when IO, StringIO
|
|
234
|
+
@source.read
|
|
235
|
+
else
|
|
236
|
+
s = @source.to_s
|
|
237
|
+
# partial loading hands us the export content itself; a path
|
|
238
|
+
# never starts with markup
|
|
239
|
+
s.lstrip.start_with?("<") ? s : File.read(s)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# --- closure ------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def closure(nodes, seeds)
|
|
246
|
+
keep = {}
|
|
247
|
+
frontier = seeds.map { |i| [i, false] }
|
|
248
|
+
until frontier.empty?
|
|
249
|
+
id, via_links = frontier.pop
|
|
250
|
+
next if keep[id]
|
|
251
|
+
|
|
252
|
+
node = nodes[id]
|
|
253
|
+
next unless node
|
|
254
|
+
# links references only buy the connector record they name;
|
|
255
|
+
# following them further pulled every relation's endpoints
|
|
256
|
+
# into a class's closure (half the model per class)
|
|
257
|
+
next if via_links && node.section != :connectors
|
|
258
|
+
|
|
259
|
+
keep[id] = true
|
|
260
|
+
node.refs.each { |r| frontier << [r, false] }
|
|
261
|
+
node.links_refs.each { |r| frontier << [r, true] }
|
|
262
|
+
# keeping a model element keeps its package ancestry
|
|
263
|
+
if node.section == :model
|
|
264
|
+
anc = nodes[node.parent_id]
|
|
265
|
+
while anc && !keep[anc.id]
|
|
266
|
+
keep[anc.id] = true
|
|
267
|
+
anc.refs.each { |r| frontier << [r, false] } unless anc.section == :model
|
|
268
|
+
anc = nodes[anc.parent_id]
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
keep_diagrams(nodes, keep)
|
|
273
|
+
keep
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# The klass tables embed each class's EA diagram; diagrams point
|
|
277
|
+
# INTO the model (their shapes carry modelElement ids), a
|
|
278
|
+
# direction the closure never walks, so diagrams whose shapes
|
|
279
|
+
# reference kept elements are kept in a second pass.
|
|
280
|
+
def keep_diagrams(nodes, keep)
|
|
281
|
+
nodes.each_value do |n|
|
|
282
|
+
next unless n.section == :diagrams && n.tag == "diagram" ||
|
|
283
|
+
(n.section == :model && n.xmi_type == "umldi:Diagram")
|
|
284
|
+
|
|
285
|
+
keep[n.id] = true if n.refs.any? { |r| keep[r] } ||
|
|
286
|
+
n.links_refs.any? { |r| keep[r] }
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def seed_ids(nodes, wanted)
|
|
291
|
+
seeds = []
|
|
292
|
+
wanted.each do |package, name|
|
|
293
|
+
named = nodes.values.select { |n| n.name == name }
|
|
294
|
+
next if named.empty?
|
|
295
|
+
|
|
296
|
+
# package-anchored matches first; consumers resolve by name,
|
|
297
|
+
# so when no package chain matches, every same-named element
|
|
298
|
+
# is seeded and the resolver picks among them as usual
|
|
299
|
+
anchored = package ? named.select { |n| under_package?(n, package, nodes) } : []
|
|
300
|
+
(anchored.empty? ? named : anchored).each { |n| seeds << n.id }
|
|
301
|
+
end
|
|
302
|
+
seeds.uniq
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def under_package?(node, package, nodes)
|
|
306
|
+
anc = nodes[node.parent_id]
|
|
307
|
+
while anc && anc.id != node.id
|
|
308
|
+
return true if anc.name == package && anc.xmi_type == "uml:Package"
|
|
309
|
+
|
|
310
|
+
anc = nodes[anc.parent_id]
|
|
311
|
+
end
|
|
312
|
+
false
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# --- pass two: re-serialize the kept subtrees, per group ----------
|
|
316
|
+
|
|
317
|
+
class GroupWriter < ::Moxml::SAX::Handler
|
|
318
|
+
def initialize(keeps, outputs)
|
|
319
|
+
@keeps = keeps
|
|
320
|
+
@outputs = outputs
|
|
321
|
+
@open = [] # [name, keys] - keys = group indexes keeping this element
|
|
322
|
+
@section = []
|
|
323
|
+
@events = 0
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def on_start_document
|
|
327
|
+
@outputs.each { |io| io << XML_DECL }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def on_start_element(name, attributes = {}, namespaces = {})
|
|
331
|
+
Slicer.gc_when_bloated if (@events += 1) % 50_000 == 0
|
|
332
|
+
section = section_for(name, @section.last)
|
|
333
|
+
@section << section
|
|
334
|
+
parent_keys = @open.empty? ? (0...@keeps.size).to_a : @open.last[1]
|
|
335
|
+
keys =
|
|
336
|
+
if section == :drop
|
|
337
|
+
[]
|
|
338
|
+
elsif GATED[section]&.include?(name)
|
|
339
|
+
id = attributes["xmi:id"] || attributes["xmi:idref"]
|
|
340
|
+
if id
|
|
341
|
+
parent_keys.select { |i| @keeps[i][id] }
|
|
342
|
+
else
|
|
343
|
+
parent_keys.dup
|
|
344
|
+
end
|
|
345
|
+
else
|
|
346
|
+
parent_keys
|
|
347
|
+
end
|
|
348
|
+
@open << [name, keys]
|
|
349
|
+
return if keys.empty?
|
|
350
|
+
|
|
351
|
+
tag = +"<#{name}"
|
|
352
|
+
namespaces.each { |p, u| tag << %( xmlns:#{p}="#{esc_attr(u)}") }
|
|
353
|
+
attributes.each { |k, v| tag << %( #{k}="#{esc_attr(v)}") }
|
|
354
|
+
tag << ">"
|
|
355
|
+
keys.each { |i| @outputs[i] << tag }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def on_end_element(name)
|
|
359
|
+
frame = @open.pop
|
|
360
|
+
@section.pop
|
|
361
|
+
frame[1].each { |i| @outputs[i] << "</#{name}>" } unless frame[1].empty?
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def on_characters(text)
|
|
365
|
+
return if @open.empty? || @open.last[1].empty?
|
|
366
|
+
|
|
367
|
+
escaped = esc_text(text)
|
|
368
|
+
@open.last[1].each { |i| @outputs[i] << escaped }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def on_cdata(text)
|
|
372
|
+
return if @open.empty? || @open.last[1].empty?
|
|
373
|
+
|
|
374
|
+
chunk = "<![CDATA[#{text}]]>"
|
|
375
|
+
@open.last[1].each { |i| @outputs[i] << chunk }
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
private
|
|
379
|
+
|
|
380
|
+
def section_for(name, current)
|
|
381
|
+
return :model if name == "uml:Model"
|
|
382
|
+
return :extension if name == "xmi:Extension"
|
|
383
|
+
|
|
384
|
+
if current == :extension
|
|
385
|
+
SECTION_CHILD[name] || :drop
|
|
386
|
+
elsif current == :drop
|
|
387
|
+
:drop
|
|
388
|
+
else
|
|
389
|
+
current
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# XML attribute-value normalization folds raw newlines and tabs
|
|
394
|
+
# into spaces, which would flatten the asciidoc block structure
|
|
395
|
+
# EA stores inside documentation attributes; character
|
|
396
|
+
# references survive normalization, so emit those instead
|
|
397
|
+
def esc_attr(value)
|
|
398
|
+
value.gsub(/[&<>"\n\r\t]/) do |c|
|
|
399
|
+
{ "&" => "&", "<" => "<", ">" => ">", '"' => """,
|
|
400
|
+
"\n" => "
", "\r" => "
", "\t" => "	" }[c]
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def esc_text(value)
|
|
405
|
+
value.gsub(/[&<>]/) do |c|
|
|
406
|
+
{ "&" => "&", "<" => "<", ">" => ">" }[c]
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
data/lib/ea/xmi.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Ea
|
|
|
9
9
|
module Xmi
|
|
10
10
|
autoload :Parser, "ea/xmi/parser"
|
|
11
11
|
autoload :LookupService, "ea/xmi/lookup_service"
|
|
12
|
+
autoload :Slicer, "ea/xmi/slicer"
|
|
13
|
+
autoload :Loader, "ea/xmi/loader"
|
|
14
|
+
autoload :Loaded, "ea/xmi/loader"
|
|
12
15
|
|
|
13
16
|
module LiquidDrops
|
|
14
17
|
autoload :RootDrop, "ea/xmi/liquid_drops/root_drop"
|
|
@@ -43,10 +46,27 @@ module Ea
|
|
|
43
46
|
# To get a `Lutaml::Uml::Document` instead (requires the optional
|
|
44
47
|
# `lutaml-uml` gem), use `Ea::Bridge::XmiToUml.transform(root)`.
|
|
45
48
|
#
|
|
46
|
-
# @param path [String] path to a .xmi file
|
|
49
|
+
# @param path [String, IO] path to a .xmi file, or an IO
|
|
50
|
+
# @param partial [Array, nil] wanted [package, name] pairs; when
|
|
51
|
+
# given, only the reference closure of those elements is parsed,
|
|
52
|
+
# assembled in memory (see Ea::Xmi::Slicer). Partial load
|
|
53
|
+
# produces the same content as whole load for everything derived
|
|
54
|
+
# from the model's elements; it exists so reading a handful of
|
|
55
|
+
# classes out of a large export does not hydrate the whole of
|
|
56
|
+
# it. Whole load is the default. package may be nil, matching by
|
|
57
|
+
# name in any package.
|
|
47
58
|
# @return [Xmi::Sparx::Root]
|
|
48
|
-
def load(path)
|
|
49
|
-
|
|
59
|
+
def load(path, partial: nil)
|
|
60
|
+
Loader.call(path, partial: partial)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Load an export into the parsed EA graph: the parser, the parsed
|
|
64
|
+
# document, and the lookup indexes the table renders resolve
|
|
65
|
+
# through. See #load for whole vs partial loading.
|
|
66
|
+
#
|
|
67
|
+
# @return [Ea::Xmi::Loaded]
|
|
68
|
+
def load_graph(path, partial: nil)
|
|
69
|
+
Loader.graph(path, partial: partial)
|
|
50
70
|
end
|
|
51
71
|
end
|
|
52
72
|
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.
|
|
4
|
+
version: 0.6.41
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -999,8 +999,10 @@ files:
|
|
|
999
999
|
- lib/ea/xmi/liquid_drops/package_drop.rb
|
|
1000
1000
|
- lib/ea/xmi/liquid_drops/root_drop.rb
|
|
1001
1001
|
- lib/ea/xmi/liquid_drops/source_target_drop.rb
|
|
1002
|
+
- lib/ea/xmi/loader.rb
|
|
1002
1003
|
- lib/ea/xmi/lookup_service.rb
|
|
1003
1004
|
- lib/ea/xmi/parser.rb
|
|
1005
|
+
- lib/ea/xmi/slicer.rb
|
|
1004
1006
|
- lib/ea/xml_escape.rb
|
|
1005
1007
|
- lib/tasks/fonts.rake
|
|
1006
1008
|
- lib/tasks/svg_bench.rake
|