Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature model metamodel extension #132

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
202 changes: 202 additions & 0 deletions besser/BUML/metamodel/feature_model/feature_model.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions docs/source/buml_language/model_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Model types
model_types/ocl
model_types/deployment
model_types/state_machine_bot
model_types/feature_model
36 changes: 36 additions & 0 deletions docs/source/buml_language/model_types/feature_model.rst
Original file line number Diff line number Diff line change
@@ -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 <structural>`.
Binary file added docs/source/img/feature_model_example.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/img/feature_model_mm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added tests/feature_model/__init__.py
Empty file.
Loading