require "nokogiri" require "htmlentities" require "lutaml/uml/has_attributes" require "lutaml/uml/document" require "lutaml/xmi" require "xmi" module Lutaml module XMI module Parsers # Class for parsing .xmi schema files into ::Lutaml::Uml::Document class XML LOWER_VALUE_MAPPINGS = { "0" => "C", "1" => "M", }.freeze attr_reader :xmi_cache, :xmi_root_model # @param xml [String] path to xml # @param options [Hash] options for parsing # @return [Lutaml::Uml::Document] def self.parse(xml, _options = {}) sparx_root = Xmi::Sparx::SparxRoot xmi_doc = Nokogiri::XML(File.read(xml)) namespace = xmi_doc.at_xpath("//xmi:XMI").namespaces if namespace["xmlns:uml"].split("/").last == "20131001" sparx_root = Xmi::Sparx::SparxRoot2013 end xml_content = File.read(xml) xmi_model = sparx_root.from_xml(xml_content) new.parse(xmi_model) end # @param xmi_model [Shale::Mapper] # @return [Lutaml::Uml::Document] def parse(xmi_model) @xmi_cache = {} @xmi_root_model = xmi_model ::Lutaml::Uml::Document.new(serialize_to_hash(xmi_model)) end private # @param xmi_model [Shale::Mapper] # @return [Hash] # @note xpath: //uml:Model[@xmi:type="uml:Model"] def serialize_to_hash(xmi_model) model = xmi_model.model { name: model.name, packages: serialize_model_packages(model), } end # @param model [Shale::Mapper] # @return [Array] # @note xpath ./packagedElement[@xmi:type="uml:Package"] def serialize_model_packages(model) model.packaged_element.select do |e| e.type?("uml:Package") end.map do |package| { xmi_id: package.id, name: get_package_name(package), classes: serialize_model_classes(package, model), enums: serialize_model_enums(package), data_types: serialize_model_data_types(package), diagrams: serialize_model_diagrams(package.id), packages: serialize_model_packages(package), definition: doc_node_attribute_value(package.id, "documentation"), stereotype: doc_node_attribute_value(package.id, "stereotype"), } end end def get_package_name(package) return package.name unless package.name.nil? connector = fetch_connector(package.id) if connector.target&.model && connector.target.model&.name return "#{connector.target.model.name} " \ "(#{package.type.split(':').last})" end "unnamed" end # @param package [Shale::Mapper] # @param model [Shale::Mapper] # @return [Array] # @note xpath ./packagedElement[@xmi:type="uml:Class" or # @xmi:type="uml:AssociationClass"] def serialize_model_classes(package, model) package.packaged_element.select do |e| e.type?("uml:Class") || e.type?("uml:AssociationClass") || e.type?("uml:Interface") end.map do |klass| { xmi_id: klass.id, name: klass.name, package: model, type: klass.type.split(":").last, attributes: serialize_class_attributes(klass), associations: serialize_model_associations(klass.id), operations: serialize_class_operations(klass), constraints: serialize_class_constraints(klass.id), is_abstract: doc_node_attribute_value(klass.id, "isAbstract"), definition: doc_node_attribute_value(klass.id, "documentation"), stereotype: doc_node_attribute_value(klass.id, "stereotype"), } end end # @param package [Shale::Mapper] # @return [Array] # @note xpath ./packagedElement[@xmi:type="uml:Enumeration"] def serialize_model_enums(package) package.packaged_element.select { |e| e.type?("uml:Enumeration") } .map do |enum| { xmi_id: enum.id, name: enum.name, values: serialize_enum_owned_literal(enum), definition: doc_node_attribute_value(enum.id, "documentation"), stereotype: doc_node_attribute_value(enum.id, "stereotype"), } end end # @param model [Shale::Mapper] # @return [Hash] # @note xpath .//ownedLiteral[@xmi:type="uml:EnumerationLiteral"] def serialize_enum_owned_literal(enum) owned_literals = enum.owned_literal.select do |owned_literal| owned_literal.type? "uml:EnumerationLiteral" end owned_literals.map do |owned_literal| # xpath .//type uml_type_id = owned_literal&.uml_type&.idref { name: owned_literal.name, type: lookup_entity_name(uml_type_id) || uml_type_id, definition: lookup_attribute_documentation(owned_literal.id), } end end # @param model [Shale::Mapper] # @return [Array] # @note xpath ./packagedElement[@xmi:type="uml:DataType"] def serialize_model_data_types(model) all_data_type_elements = [] select_all_packaged_elements(all_data_type_elements, model, "uml:DataType") all_data_type_elements.map do |klass| { xmi_id: klass.id, name: klass.name, attributes: serialize_class_attributes(klass), operations: serialize_class_operations(klass), associations: serialize_model_associations(klass.id), constraints: serialize_class_constraints(klass.id), is_abstract: doc_node_attribute_value(klass.id, "isAbstract"), definition: doc_node_attribute_value(klass.id, "documentation"), stereotype: doc_node_attribute_value(klass.id, "stereotype"), } end end # @param node_id [String] # @return [Array] # @note xpath %(//diagrams/diagram/model[@package="#{node['xmi:id']}"]) def serialize_model_diagrams(node_id) diagrams = @xmi_root_model.extension.diagrams.diagram.select do |d| d.model.package == node_id end diagrams.map do |diagram| { xmi_id: diagram.id, name: diagram.properties.name, definition: diagram.properties.documentation, } end end # @param xmi_id [String] # @return [Array] # @note xpath %(//element[@xmi:idref="#{xmi_id}"]/links/*) def serialize_model_associations(xmi_id) matched_element = @xmi_root_model.extension.elements.element .find { |e| e.idref == xmi_id } return if !matched_element || !matched_element.links || matched_element.links.association.empty? matched_element.links.association.map do |assoc| link_member = assoc.start == xmi_id ? "end" : "start" linke_owner_name = link_member == "start" ? "end" : "start" member_end, member_end_type, member_end_cardinality, member_end_attribute_name, member_end_xmi_id = serialize_member_type(xmi_id, assoc, link_member) owner_end = serialize_owned_type(xmi_id, assoc, linke_owner_name) if member_end && ((member_end_type != "aggregation") || (member_end_type == "aggregation" && member_end_attribute_name)) doc_node_name = (link_member == "start" ? "source" : "target") definition = fetch_definition_node_value(assoc.id, doc_node_name) { xmi_id: assoc.id, member_end: member_end, member_end_type: member_end_type, member_end_cardinality: member_end_cardinality, member_end_attribute_name: member_end_attribute_name, member_end_xmi_id: member_end_xmi_id, owner_end: owner_end, owner_end_xmi_id: xmi_id, definition: definition, } end end end # @param link_id [String] # @return [Shale::Mapper] # @note xpath %(//connector[@xmi:idref="#{link_id}"]) def fetch_connector(link_id) @xmi_root_model.extension.connectors.connector.select do |con| con.idref == link_id end.first end # @param link_id [String] # @param node_name [String] source or target # @return [String] # @note xpath # %(//connector[@xmi:idref="#{link_id}"]/#{node_name}/documentation) def fetch_definition_node_value(link_id, node_name) connector_node = fetch_connector(link_id) connector_node.send(node_name.to_sym).documentation end # @param klass [Shale::Mapper] # @return [Array] # @note xpath .//ownedOperation def serialize_class_operations(klass) klass.owned_operation.map do |operation| uml_type = operation.uml_type.first uml_type_idref = uml_type.idref if uml_type if operation.association.nil? { id: operation.id, xmi_id: uml_type_idref, name: operation.name, definition: lookup_attribute_documentation(operation.id), } end end.compact end # @param klass_id [String] # @return [Array] # @note xpath ./constraints/constraint def serialize_class_constraints(klass_id) connector_node = fetch_connector(klass_id) if connector_node # In ea-xmi-2.5.1, constraints are moved to source/target under # connectors constraints = %i[source target].map do |st| connector_node.send(st).constraints.constraint end.flatten constraints.map do |constraint| { name: HTMLEntities.new.decode(constraint.name), type: constraint.type, weight: constraint.weight, status: constraint.status, } end end end # @param owner_xmi_id [String] # @param link [Shale::Mapper] # @param link_member_name [String] # @return [String] def serialize_owned_type(owner_xmi_id, link, linke_owner_name) case link.name when "NoteLink" return when "Generalization" return generalization_association(owner_xmi_id, link) end xmi_id = link.send(linke_owner_name.to_sym) lookup_entity_name(xmi_id) || connector_source_name(xmi_id) # not necessary # if link.name == "Association" # owned_cardinality, owned_attribute_name = # fetch_assoc_connector(link.id, "source") # else # owned_cardinality, owned_attribute_name = # fetch_owned_attribute_node(xmi_id) # end # [owner_end, owned_cardinality, owned_attribute_name] # owner_end end # @param owner_xmi_id [String] # @param link [Shale::Mapper] # @return [Array] def serialize_member_end(owner_xmi_id, link) case link.name when "NoteLink" return when "Generalization" return generalization_association(owner_xmi_id, link) end xmi_id = link.start source_or_target = :source if link.start == owner_xmi_id xmi_id = link.end source_or_target = :target end member_end = lookup_entity_name(xmi_id) || connector_name_by_source_or_target(xmi_id, source_or_target) [member_end, xmi_id] end # @param owner_xmi_id [String] # @param link [Shale::Mapper] # @param link_member_name [String] # @return [Array] def serialize_member_type(owner_xmi_id, link, link_member_name) member_end, xmi_id = serialize_member_end(owner_xmi_id, link) if link.name == "Association" connector_type = link_member_name == "start" ? "source" : "target" member_end_cardinality, member_end_attribute_name = fetch_assoc_connector(link.id, connector_type) else member_end_cardinality, member_end_attribute_name = fetch_owned_attribute_node(xmi_id) end [member_end, "aggregation", member_end_cardinality, member_end_attribute_name, xmi_id] end # @param link_id [String] # @param connector_type [String] # @return [Array] # @note xpath %(//connector[@xmi:idref="#{link_id}"]/#{connector_type}) def fetch_assoc_connector(link_id, connector_type) assoc_connector = fetch_connector(link_id).send(connector_type.to_sym) if assoc_connector assoc_connector_type = assoc_connector.type if assoc_connector_type&.multiplicity cardinality = assoc_connector_type.multiplicity.split("..") cardinality.unshift("1") if cardinality.length == 1 min, max = cardinality end assoc_connector_role = assoc_connector.role # Does role has name attribute? Or get name from model? # attribute_name = assoc_connector_role.name if assoc_connector_role attribute_name = assoc_connector.model.name if assoc_connector_role cardinality = cardinality_min_max_value(min, max) end [cardinality, attribute_name] end # @param owner_xmi_id [String] # @param link [Shale::Mapper] # @return [Array] # @note match return value of serialize_member_type def generalization_association(owner_xmi_id, link) member_end_type = "generalization" xmi_id = link.start source_or_target = :source if link.start == owner_xmi_id member_end_type = "inheritance" xmi_id = link.end source_or_target = :target end member_end = lookup_entity_name(xmi_id) || connector_name_by_source_or_target(xmi_id, source_or_target) member_end_cardinality, _member_end_attribute_name = fetch_owned_attribute_node(xmi_id) [member_end, member_end_type, member_end_cardinality, nil, xmi_id] end # Multiple items if search type is idref. Should search association? # @param xmi_id [String] # @return [Array] # @note xpath # %(//ownedAttribute[@association]/type[@xmi:idref="#{xmi_id}"]) def fetch_owned_attribute_node(xmi_id) all_elements = all_packaged_elements owned_attributes = all_elements.map(&:owned_attribute).flatten oa = owned_attributes.select do |a| !!a.association && a.uml_type && a.uml_type.idref == xmi_id end.first if oa cardinality = cardinality_min_max_value( oa.lower_value&.value, oa.upper_value&.value ) oa_name = oa.name end [cardinality, oa_name] end # @param klass_id [String] # @return [Shale::Mapper] # @note xpath %(//element[@xmi:idref="#{klass['xmi:id']}"]) def fetch_element(klass_id) @xmi_root_model.extension.elements.element.select do |e| e.idref == klass_id end.first end # @param klass [Shale::Mapper] # @return [Array] # @note xpath .//ownedAttribute[@xmi:type="uml:Property"] def serialize_class_attributes(klass) klass.owned_attribute.select { |attr| attr.type?("uml:Property") } .map do |oa| uml_type = oa.uml_type uml_type_idref = uml_type.idref if uml_type if oa.association.nil? { id: oa.id, name: oa.name, type: lookup_entity_name(uml_type_idref) || uml_type_idref, xmi_id: uml_type_idref, is_derived: oa.is_derived, cardinality: cardinality_min_max_value( oa.lower_value&.value, oa.upper_value&.value, ), definition: lookup_attribute_documentation(oa.id), } end end.compact end # @param min [String] # @param max [String] # @return [Hash] def cardinality_min_max_value(min, max) { "min" => cardinality_value(min, true), "max" => cardinality_value(max, false), } end # @param value [String] # @param is_min [Boolean] # @return [String] def cardinality_value(value, is_min = false) return unless value is_min ? LOWER_VALUE_MAPPINGS[value.to_s] : value end # @node [Shale::Mapper] # @attr_name [String] # @return [String] # @note xpath %(//element[@xmi:idref="#{xmi_id}"]/properties) def doc_node_attribute_value(node_id, attr_name) doc_node = fetch_element(node_id) return unless doc_node doc_node.properties&.send(Shale::Utils.snake_case(attr_name).to_sym) end # @param xmi_id [String] # @return [Shale::Mapper] # @note xpath %(//attribute[@xmi:idref="#{xmi_id}"]) def fetch_attribute_node(xmi_id) attribute_node = nil @xmi_root_model.extension.elements.element.each do |e| if e.attributes&.attribute e.attributes.attribute.each do |a| attribute_node = a if a.idref == xmi_id end end end attribute_node end # @param xmi_id [String] # @return [String] # @note xpath %(//attribute[@xmi:idref="#{xmi_id}"]/documentation) def lookup_attribute_documentation(xmi_id) attribute_node = fetch_attribute_node(xmi_id) return unless attribute_node&.documentation attribute_node&.documentation&.value end # @param xmi_id [String] # @return [String] def lookup_entity_name(xmi_id) model_node_name_by_xmi_id(xmi_id) if @xmi_cache.empty? @xmi_cache[xmi_id] end # @param xmi_id [String] # @param source_or_target [String] # @return [String] def connector_name_by_source_or_target(xmi_id, source_or_target) node = @xmi_root_model.extension.connectors.connector.select do |con| con.send(source_or_target.to_sym).idref == xmi_id end return if node.empty? || node.first.send(source_or_target.to_sym).nil? || node.first.send(source_or_target.to_sym).model.nil? node.first.send(source_or_target.to_sym).model.name end # @param xmi_id [String] # @return [String] # @note xpath %(//source[@xmi:idref="#{xmi_id}"]/model) def connector_source_name(xmi_id) connector_name_by_source_or_target(xmi_id, :source) end # @param xmi_id [String] # @return [String] # @note xpath %(//target[@xmi:idref="#{xmi_id}"]/model) def connector_target_name(xmi_id) connector_name_by_source_or_target(xmi_id, :target) end # @param xmi_id [String] # @return [String] # @note xpath %(//*[@xmi:id="#{xmi_id}"]) def model_node_name_by_xmi_id(xmi_id) id_name_mapping = Hash.new map_id_name(id_name_mapping, @xmi_root_model) @xmi_cache = id_name_mapping @xmi_cache[xmi_id] end # @return [Array] def all_packaged_elements all_elements = [] packaged_element_roots = @xmi_root_model.model.packaged_element + @xmi_root_model.extension.primitive_types.packaged_element + @xmi_root_model.extension.profiles.profile.map(&:packaged_element) packaged_element_roots.flatten.each do |e| select_all_packaged_elements(all_elements, e, nil) end all_elements end # @param items [Array] # @param model [Shale::Mapper] # @param type [String] nil for any def select_all_items(items, model, type, method) iterate_tree(items, model, type, method.to_sym) end # @param all_elements [Array] # @param model [Shale::Mapper] # @param type [String] nil for any # @note xpath ./packagedElement[@xmi:type="#{type}"] def select_all_packaged_elements(all_elements, model, type) select_all_items(all_elements, model, type, :packaged_element) all_elements.delete_if do |e| !e.is_a?(Xmi::Uml::PackagedElement) && !e.is_a?(Xmi::Uml::PackagedElement2013) end end # @param result [Array] # @param node [Shale::Mapper] # @param type [String] nil for any # @param children_method [String] method to determine children exist def iterate_tree(result, node, type, children_method) result << node if type.nil? || node.type == type return unless node.send(children_method.to_sym) node.send(children_method.to_sym).each do |sub_node| if sub_node.send(children_method.to_sym) iterate_tree(result, sub_node, type, children_method) elsif type.nil? || sub_node.type == type result << sub_node end end end # @param result [Hash] # @param node [Shale::Mapper] # @note set id as key and name as value into result # if id and name are found def map_id_name(result, node) return if node.nil? if node.is_a?(Array) node.each do |arr_item| map_id_name(result, arr_item) end elsif node.class.methods.include?(:attributes) attrs = node.class.attributes if attrs.has_key?(:id) && attrs.has_key?(:name) result[node.id] = node.name end attrs.each_pair do |k, _v| map_id_name(result, node.send(k)) end end end end end end end