From 91aa6da682a1d4c45267687276deb38e29e46486 Mon Sep 17 00:00:00 2001 From: mgv99 Date: Tue, 10 Dec 2024 11:13:53 +0100 Subject: [PATCH 1/2] add feature model metamodel extension --- .../BUML/metamodel/feature_model/__init__.py | 0 .../metamodel/feature_model/feature_model.py | 202 ++++++++++++++++++ docs/source/buml_language/model_types.rst | 1 + .../model_types/feature_model.rst | 36 ++++ docs/source/img/feature_model_example.jpg | Bin 0 -> 26769 bytes docs/source/img/feature_model_mm.png | Bin 0 -> 118909 bytes tests/feature_model/__init__.py | 0 tests/feature_model/test_feature_model.py | 165 ++++++++++++++ 8 files changed, 404 insertions(+) create mode 100644 besser/BUML/metamodel/feature_model/__init__.py create mode 100644 besser/BUML/metamodel/feature_model/feature_model.py create mode 100644 docs/source/buml_language/model_types/feature_model.rst create mode 100644 docs/source/img/feature_model_example.jpg create mode 100644 docs/source/img/feature_model_mm.png create mode 100644 tests/feature_model/__init__.py create mode 100644 tests/feature_model/test_feature_model.py diff --git a/besser/BUML/metamodel/feature_model/__init__.py b/besser/BUML/metamodel/feature_model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/besser/BUML/metamodel/feature_model/feature_model.py b/besser/BUML/metamodel/feature_model/feature_model.py new file mode 100644 index 00000000..482e8acc --- /dev/null +++ b/besser/BUML/metamodel/feature_model/feature_model.py @@ -0,0 +1,202 @@ +from besser.BUML.metamodel.structural import Model, NamedElement, Element + +MANDATORY = 'mandatory' +OPTIONAL = 'optional' +OR = 'or' +ALTERNATIVE = 'alternative' + + +class FeatureValue(Element): + + def __init__(self, t: str, values: list = None, min: float = None, max: float = None): + if ((min or max) and not values) or (not min and not max): + if t == 'int': + if values and any(map(lambda x: not isinstance(x, int), values)): + raise ValueError('Value must be an integer') + if t == 'float': + if values and any(map(lambda x: not isinstance(x, float), values)): + raise ValueError('Value must be a float') + if t == 'str': + if values and any(map(lambda x: not isinstance(x, str), values)): + raise ValueError(' Value must be a string') + else: + raise ValueError('Invalid arguments') + self.type: str = t + self.values: list = values + self.min: int = min + self.max: int = max + + def __eq__(self, other): + if type(other) is type(self): + return self.type == other.type and self.values == other.values and self.min == other.min and self.max == other.max + else: + return False + + +class Feature(NamedElement): + + @staticmethod + def duplicate(f: 'Feature', parent: 'Feature' = None, min: int = 1, max: int = 1) -> 'Feature': + new_f = Feature(f.name, min=min, max=max, value=f.value) + new_f.parent = parent + for children_group in f.children_groups: + new_f.children_groups.append(children_group.duplicate(new_f)) + return new_f + + def __init__(self, name: str, min: int = 1, max: int = 1, value: FeatureValue = None): + super().__init__(name) + if min > max or min < 1: + raise ValueError(f'Error in {name}: 0 < min < max') + self.min: int = min + self.max: int = max + self.value: FeatureValue = value + self.parent: Feature = None + self.children_groups: list[FeatureGroup] = [] + + def __eq__(self, other): + if type(other) is type(self): + return self.name == other.name and self.min == other.min and self.max == other.max and self.value == other.value and self.children_groups == other.children_groups + else: + return False + + def to_json(self): + d = [] + for children_group in self.children_groups: + g = {'kind': children_group.kind, 'features': []} + d.append(g) + for feature in children_group.features: + g['features'].append(feature.to_json()) + return {self.name: d} + + def mandatory(self, child: 'Feature') -> 'Feature': + if child.parent is not None: + raise ValueError(f'Feature {child.name} cannot be a child of {self.name}. It has feature {child.parent.name} as parent.') + self.children_groups.append(FeatureGroup(MANDATORY, [child])) + child.parent = self + return self + + def optional(self, child: 'Feature') -> 'Feature': + if child.parent is not None: + raise ValueError(f'Feature {child.name} cannot be a child of {self.name}. It has feature {child.parent.name} as parent.') + self.children_groups.append(FeatureGroup(OPTIONAL, [child])) + child.parent = self + return self + + def alternative(self, children: list['Feature']) -> 'Feature': + for child in children: + if child.parent is not None: + raise ValueError(f'Feature {child.name} cannot be a child of {self.name}. It has feature {child.parent.name} as parent.') + child.parent = self + self.children_groups.append(FeatureGroup(ALTERNATIVE, children)) + return self + + def or_(self, children: list['Feature']) -> 'Feature': + for child in children: + if child.parent is not None: + raise ValueError(f'Feature {child.name} cannot be a child of {self.name}. It has feature {child.parent.name} as parent.') + child.parent = self + self.children_groups.append(FeatureGroup(OR, children)) + return self + + def get_depth(self, depth: int = 0) -> int: + max_depth = depth + for children_group in self.children_groups: + for child in children_group.features: + child_depth = child.get_depth(depth+1) + if child_depth > max_depth: + max_depth = child_depth + return max_depth + + +class FeatureGroup(Element): + + def __init__(self, kind: str, features: list[Feature] = []): + if (kind == MANDATORY or kind == OPTIONAL) and len(features) > 1: + raise ValueError(f'{kind} has more than 1 feature') + if (kind == ALTERNATIVE or kind == OR) and len(features) < 2: + raise ValueError(f'{kind} has less than 2 features') + + self.features: list[Feature] = features + self.kind: str = kind + + def __eq__(self, other): + if type(other) is type(self): + return self.kind == other.kind and self.features == other.features + else: + return False + + def duplicate(self, parent: Feature) -> 'FeatureGroup': + new_children: list[Feature] = [] + for f in self.features: + new_children.append(Feature.duplicate(f, parent, min=f.min, max=f.max)) + return FeatureGroup(self.kind, new_children) + + +class FeatureConfiguration(Element): + + def __init__(self, feature: Feature): + self.feature: Feature = feature + self.value: int or float or str = None + self.parent: FeatureConfiguration = None + self.children: list[FeatureConfiguration] = [] + + def to_json(self): + c = [] + for child in self.children: + c.append(child.to_json()) + if c: + if len(c) == 1: + return {self.feature.name: c[0]} + return {self.feature.name: c} + elif self.value is not None: + return {self.feature.name: self.value} + else: + return self.feature.name + + def add_child(self, child: 'FeatureConfiguration') -> None: + child.parent = self + self.children.append(child) + + def add_children(self, children: list['FeatureConfiguration']) -> None: + for child in children: + child.parent = self + self.children.extend(children) + + def get_child(self, name: str) -> 'FeatureConfiguration': + child = [c for c in self.children if c.feature.name == name] + if len(child) > 1: + raise ValueError(f"Feature {self.feature.name} has {len(child)} children with the name {name}. Make sure there are no more than one children with the same name") + if len(child) == 0: + return None + return child[0] + + def get_children(self, name: str) -> list['FeatureConfiguration']: + return [c for c in self.children if c.feature.name == name] + + def get_depth(self, depth: int = 0) -> int: + max_depth = depth + for child in self.children: + child_depth = child.get_depth(depth+1) + if child_depth > max_depth: + max_depth = child_depth + return max_depth + + +class FeatureModel(Model): + + def __init__(self, name: str): + super().__init__(name) + self.root_feature: Feature = None + + def __eq__(self, other): + if type(other) is type(self): + return self.name == other.name and self.root_feature == other.root_feature + else: + return False + + def root(self, feature: Feature) -> 'FeatureModel': + self.root_feature = feature + return self + + def duplicate(self, min: int = 1, max: int = 1) -> Feature: + return Feature.duplicate(f=self.root_feature, min=min, max=max) diff --git a/docs/source/buml_language/model_types.rst b/docs/source/buml_language/model_types.rst index bcb75a79..9dc66660 100644 --- a/docs/source/buml_language/model_types.rst +++ b/docs/source/buml_language/model_types.rst @@ -10,3 +10,4 @@ Model types model_types/ocl model_types/deployment model_types/state_machine_bot + model_types/feature_model diff --git a/docs/source/buml_language/model_types/feature_model.rst b/docs/source/buml_language/model_types/feature_model.rst new file mode 100644 index 00000000..ffe8e8eb --- /dev/null +++ b/docs/source/buml_language/model_types/feature_model.rst @@ -0,0 +1,36 @@ +Feature Model +============= + +This metamodel allows the definition of feature models. + +Feature models in software product lines are structured representations of the features (functionalities or characteristics) +of a system, capturing their variability and dependencies. They are used to model the commonalities and differences among +products in a product line, typically organized hierarchically with constraints to specify valid combinations of features. + +Feature configurations are specific selections of features from a feature model that represent a valid product within +the software product line. They are created by choosing features while adhering to the constraints and dependencies +defined in the model, such as mandatory or optional features. + +.. figure:: ../../img/feature_model_example.jpg + :width: 500 + :alt: Example Feature Model + :align: center + + Example Feature Model + +In BESSER, you can create Feature Models with the following properties: + +- Cardinality-based features: set the minimum and/or maximum number of instances of a feature +- Attributes: a feature can have an associated value to be filled during the configuration definition +- Modularity: attach feature models into other feature models, allowing reusability of existing feature models. + +The following figure shows the metamodel diagram: + +.. image:: ../../img/feature_model_mm.png + :width: 800 + :alt: Feature Model metamodel + :align: center + +.. note:: + + The classes highlighted in green originate from the :doc:`structural metamodel `. diff --git a/docs/source/img/feature_model_example.jpg b/docs/source/img/feature_model_example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ee9131c43f493ab576cd77dc6723f217b503deb4 GIT binary patch literal 26769 zcmce82|SeT`|l$}wz2OF5elhnB@D@yY$>u%h0w$#+sH5?`&KE6mm!tPmc4|@9+I8x zW@L#lV|_AX<~;BJobx;X-~0Z~Ilps0pK~Vn=hHlC0&oCeVufb==Y;>;i|GI~$AfI_hd4N)4^;C42bh?d53n%*IcwCxa#QS?%{dU%iHHxKwwaC2qyGi^n;k#xcG#`$LUX= zW@J8lp7o;O<*V0)Z{EJEsI024sr~S=uDPYPt-a$*XV<{s(D2CU_pxyzY3|4T!s61; zWy;3p*7nXW_1pdV%c*No`JmG?H!NMa9wJ|s)D6@_)Jx)|eiaY(I{KvWx%+bql&IU^tb zHdyHHS1Un4{qunaX1b`+Z0|mkt|0@6$*pDpoll49oiiIK#6bq|>ta7)D^`5l3HV*> zw4V~egt>M4dm}wdBb|@cD&J}p#!4@3eWTD9k|Nfgya@eS0noqb3Ig8nr3K? z*XgO6t+~^B;oNGgjxT#lv6sLjo@k=0M0=S+!8-=fnu+|w$mg^;Hcq8oEY?9H)@Gxi z&}oIn$%Q+#B^BZ{MCV`1W&fRLT?rmw8!kl~-q6Ra{(6CXSQD`Vh^2}CU{0kd{tBb& zgIZ1KAj%u>;ESY_0dFz84zf@P5I6T_ey~hc=JCS0%sU+_$NS8W zrk+>0`s7sWhuaSLCBtoQ3*t;%J?raEGNDxm*XnA|ZDBlIUviFH*hjm{MpVrSAOB?q_uCTP+AJ5!0lIsB<;EB-)5%#3RIm#^G;(rNU;05mTx-BmZkYWR-QyXnt1iebz;hJ(WIgsf4J*` z&f|Qecsj77YiY$^cnE5wdC@)3xqMp8v(WoD=EB`z_58{rq$ zukJsl3r-`aEW<*y?%y78aWV6F^lj3kTpr)vfNaBW99uzb&ui0@Zf(H6JJ2sO0HWf4 z(hdXYiqsoPRLVQ>|6_c#PC{&4pm{QYYtA$KyxYSg$V3ed3L-WrT-@A=Wk_YA%IC62 z-XjiS=02RtduCtgX!2rtDO&Gv{C~6oruf`Vfl>n!)5~W}*I%=!R?!E;}&??s0 z5L&t&#ZSwoxTjc16qj@4d2FEE>4IsiM;L&qb|GZ%$0`Q!BMaDO010q58iHaK&cApy z+SqIItDV|59VnUrYkIYX8~8Yj8T3dDs3`Fj>)vhBhi@Kvld$~Fu= z0?u@)Y<#d=JBV7ui(^9O;4Rs<0VKkNK=ZU%t?~!+B2dn{nk6hW(;X3mneSa?o}*IZ zs>tZoGjC~D1iNU|EIwM!2EGekd=E{@O6Ly_fQlrUe#UWOtwxG9C&o0xMOrgXWTV0Z zTE<`Q1(80RyL0MZ)cvwyK2D63e-veTl|1vA0kDBK-X(=UHBaJV4=_G>_&P}q